diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index e639d926..f1918459 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -20,11 +20,17 @@ env: SCL_DEPENDENCY_TRACKER_SERVER_URL: ${{ secrets.SCL_DEPENDENCY_TRACKER_SERVER_URL }} SCL_DEPENDENCY_TRACKER_SIGNING_PRIVATE_KEY: ${{ secrets.SCL_DEPENDENCY_TRACKER_SIGNING_PRIVATE_KEY }} - BINDINGS_IOS___SDK_VERSION: "17.5" - BINDINGS_IOS___XCODE_IDE_DEV_PATH: "/Applications/Xcode_15.4.app/Contents/Developer" + DOTNET_TARGET_WORKLOAD_VERSION: "8.0.402" # dont upgrade this lightheartedly the workload snapshot implicitly defines which versions of Android/iOS/MacCatalyst SDKs are supported - BINDINGS_MACCATALYST___SDK_VERSION: "14.5" - BINDINGS_MACCATALYST___XCODE_IDE_DEV_PATH: "/Applications/Xcode_15.4.app/Contents/Developer" + BINDINGS_ANDROID___DOTNET_TARGET_PLATFORM_VERSION: "34" # for the csproj file + + BINDINGS_IOS___SDK_VERSION: "17.5" # for xcodebuild + BINDINGS_IOS___XCODE_IDE_DEV_PATH: "/Applications/Xcode_15.4.app/Contents/Developer" # for xcodebuild + BINDINGS_IOS___DOTNET_TARGET_PLATFORM_VERSION: "17.0" # for the csproj file + + BINDINGS_MACCATALYST___SDK_VERSION: "14.5" # for xcodebuild + BINDINGS_MACCATALYST___XCODE_IDE_DEV_PATH: "/Applications/Xcode_15.4.app/Contents/Developer" # for xcodebuild + BINDINGS_MACCATALYST___DOTNET_TARGET_PLATFORM_VERSION: "17.0" # for the csproj file on: workflow_call: # so that other workflows can trigger this @@ -64,6 +70,7 @@ jobs: chmod +x "${{env.BUILD_REPOSITORY_FOLDERPATH}}/Laerdal.Scripts/Laerdal.SetupBuildEnvironment.sh" \ && \ "${{env.BUILD_REPOSITORY_FOLDERPATH}}/Laerdal.Scripts/Laerdal.SetupBuildEnvironment.sh" \ + "${{env.DOTNET_TARGET_WORKLOAD_VERSION}}" \ "https://nuget.pkg.github.com/Laerdal/index.json" \ "${{env.SCL_GITHUB_NUGET_FEED_USERNAME}}" \ "${{env.SCL_GITHUB_ACCESS_TOKEN}}" \ @@ -85,26 +92,31 @@ jobs: -p:Should_Skip_MacCatalyst="false" \ \ -p:PackageOutputPath="${{env.BUILD_REPOSITORY_FOLDERPATH}}/Artifacts" \ + \ -p:Laerdal_Gradle_Path="/opt/homebrew/opt/gradle@7/bin/gradle" \ -p:Laerdal_Source_Branch="${{env.LAERDAL_SOURCE_BRANCH}}" \ -p:Laerdal_Repository_Path="${{env.LAERDAL_REPOSITORY_PATH}}" \ -p:Laerdal_Github_Access_Token="${{env.SCL_GITHUB_ACCESS_TOKEN}}" \ -p:Laerdal_Test_Results_Folderpath="${{env.BUILD_REPOSITORY_FOLDERPATH}}/TestResults" \ \ - -p:Laerdal_Dependency_Tracker_Server_Url="${{env.SCL_DEPENDENCY_TRACKER_SERVER_URL}}" \ - -p:Laerdal_Dependency_Tracker_Api_Key_File_Path="${{env.BUILD_REPOSITORY_FOLDERPATH}}/Laerdal.Scripts/dependency_tracker_api_key.ppk" \ - -p:Laerdal_Dependency_Tracker_Private_Signing_Key_File_Path="${{env.BUILD_REPOSITORY_FOLDERPATH}}/Laerdal.Scripts/dependency_tracker_private_signing_key.ppk" \ + -p:Laerdal_Bindings_Android___DotnetTargetPlatformVersion="${{env.BINDINGS_ANDROID___DOTNET_TARGET_PLATFORM_VERSION}}" \ \ -p:Laerdal_Bindings_iOS___Sdk_Version="${{env.BINDINGS_IOS___SDK_VERSION}}" \ -p:Laerdal_Bindings_iOS___Xcode_Ide_Dev_Path="${{env.BINDINGS_IOS___XCODE_IDE_DEV_PATH}}" \ + -p:Laerdal_Bindings_iOS___DotnetTargetPlatformVersion="${{env.BINDINGS_IOS___DOTNET_TARGET_PLATFORM_VERSION}}" \ \ -p:Laerdal_Bindings_MacCatalyst___Sdk_Version="${{env.BINDINGS_MACCATALYST___SDK_VERSION}}" \ -p:Laerdal_Bindings_MacCatalyst___Xcode_Ide_Dev_Path="${{env.BINDINGS_MACCATALYST___XCODE_IDE_DEV_PATH}}" \ + -p:Laerdal_Bindings_MacCatalyst___DotnetTargetPlatformVersion="${{env.BINDINGS_MACCATALYST___DOTNET_TARGET_PLATFORM_VERSION}}" \ + \ + -p:Laerdal_Dependency_Tracker_Server_Url="${{env.SCL_DEPENDENCY_TRACKER_SERVER_URL}}" \ + -p:Laerdal_Dependency_Tracker_Api_Key_File_Path="${{env.BUILD_REPOSITORY_FOLDERPATH}}/Laerdal.Scripts/dependency_tracker_api_key.ppk" \ + -p:Laerdal_Dependency_Tracker_Private_Signing_Key_File_Path="${{env.BUILD_REPOSITORY_FOLDERPATH}}/Laerdal.Scripts/dependency_tracker_private_signing_key.ppk" \ && \ rm "./dependency_tracker_private_signing_key.ppk" "./dependency_tracker_api_key.ppk" - name: '📡 Publish Test Results' # https://github.com/marketplace/actions/publish-test-results - uses: 'EnricoMi/publish-unit-test-result-action/macos@v2' + uses: 'EnricoMi/publish-unit-test-result-action/macos@master' if: always() with: files: | diff --git a/Laerdal.McuMgr.Bindings.Android.Native/gradle.properties b/Laerdal.McuMgr.Bindings.Android.Native/gradle.properties index ab4b134e..123e09cf 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/gradle.properties +++ b/Laerdal.McuMgr.Bindings.Android.Native/gradle.properties @@ -19,7 +19,9 @@ android.useAndroidX=true # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -# in macos just comment this one out +# as long as we use gradle 7.6 we will get warnings if we try to build against android 34 or above we suppress such warnings +android.suppressUnsupportedCompileSdk=34 +# in macos just comment out the windows paths and keep the homebrew path # org.gradle.java.home=C/://Program Files//Microsoft//jdk-11.0.15.10-hotspot # org.gradle.java.home=C://Program Files//OpenJDK//jdk-17.0.2 org.gradle.java.home=/opt/homebrew/opt/openjdk@17 diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/build.gradle b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/build.gradle index 1243cea6..aad9008a 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/build.gradle +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/build.gradle @@ -7,11 +7,11 @@ tasks.register('wrapper', Wrapper) { } android { - compileSdk 33 + compileSdk 34 // its best to keep this always aligned with the in the .csproj file and with BINDINGS_ANDROID___DOTNET_TARGET_PLATFORM_VERSION at the top of the github actions .yml file defaultConfig { minSdk 21 - targetSdk 33 + targetSdk 34 // its best to keep this always aligned with the in the .csproj file and with BINDINGS_ANDROID___DOTNET_TARGET_PLATFORM_VERSION at the top of the github actions .yml file consumerProguardFiles "consumer-rules.pro" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -38,12 +38,12 @@ android { } dependencies { - implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.1' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'no.nordicsemi.android:mcumgr-ble:2.0.2' - implementation 'no.nordicsemi.android:mcumgr-core:2.0.2' - implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'no.nordicsemi.android:mcumgr-ble:2.2.1' + implementation 'no.nordicsemi.android:mcumgr-core:2.2.1' + implementation 'com.google.android.material:material:1.12.0' } // repositories { diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidDeviceResetter.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidDeviceResetter.java index c6d8e867..eaafd3eb 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidDeviceResetter.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidDeviceResetter.java @@ -4,15 +4,19 @@ import android.content.Context; import androidx.annotation.NonNull; import io.runtime.mcumgr.McuMgrCallback; +import io.runtime.mcumgr.McuMgrErrorCode; import io.runtime.mcumgr.McuMgrTransport; import io.runtime.mcumgr.ble.McuMgrBleTransport; import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.managers.DefaultManager; +import io.runtime.mcumgr.response.HasReturnCode; import io.runtime.mcumgr.response.dflt.McuMgrOsResponse; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @SuppressWarnings("unused") -public class AndroidDeviceResetter { +public class AndroidDeviceResetter +{ private DefaultManager _manager; private final McuMgrTransport _transport; @@ -23,49 +27,59 @@ public class AndroidDeviceResetter { * @param context the android-context of the calling environment * @param bluetoothDevice the device to perform the firmware-install on */ - public AndroidDeviceResetter(@NonNull final Context context, @NonNull final BluetoothDevice bluetoothDevice) { + public AndroidDeviceResetter(@NonNull final Context context, @NonNull final BluetoothDevice bluetoothDevice) + { _transport = new McuMgrBleTransport(context, bluetoothDevice); } - public void beginReset() { + public EAndroidDeviceResetterInitializationVerdict beginReset() + { + if (!IsCold()) + { //keep first + onError("[ADR.BR.000] Another reset operation is already in progress (state='" + _currentState + "')"); + + return EAndroidDeviceResetterInitializationVerdict.FAILED__OTHER_RESET_ALREADY_IN_PROGRESS; + } + try { - _manager = new DefaultManager(_transport); + setState(EAndroidDeviceResetterState.IDLE); //order + _manager = new DefaultManager(_transport); //order + setState(EAndroidDeviceResetterState.RESETTING); //order + + AndroidDeviceResetter self = this; + _manager.reset(new McuMgrCallback() + { + @Override + public void onResponse(@NotNull final McuMgrOsResponse response) + { + if (!response.isSuccess()) + { // check for an error return code + self.onError("[ADR.BR.002] Reset failed (error-code '" + response.getReturnCode().toString() + "')", response.getReturnCode(), response.getGroupReturnCode()); + return; + } + + setState(EAndroidDeviceResetterState.COMPLETE); + } + + @Override + public void onError(@NotNull final McuMgrException exception) + { + self.onError("[ADR.BR.005] Reset failed '" + exception.getMessage() + "'", exception); + } + }); } catch (final Exception ex) { - setState(EAndroidDeviceResetterState.FAILED); - fatalErrorOccurredAdvertisement("Failed to create manager: '" + ex.getMessage() + "'"); - return; + onError("[ADR.BR.010] Failed to initialize reset operation: '" + ex.getMessage() + "'", ex); + return EAndroidDeviceResetterInitializationVerdict.FAILED__ERROR_UPON_COMMENCING; } - setState(EAndroidDeviceResetterState.RESETTING); - - _manager.reset(new McuMgrCallback() { - - @Override - public void onResponse(@NotNull final McuMgrOsResponse response) { - if (!response.isSuccess()) { // check for an error return code - fatalErrorOccurredAdvertisement("Reset failed (error-code '" + response.getReturnCode().toString() + "')"); - - setState(EAndroidDeviceResetterState.FAILED); - return; - } - - setState(EAndroidDeviceResetterState.COMPLETE); - } - - @Override - public void onError(@NotNull final McuMgrException error) { - fatalErrorOccurredAdvertisement("Reset failed '" + error.getMessage() + "'"); - - setState(EAndroidDeviceResetterState.FAILED); - } - - }); + return EAndroidDeviceResetterInitializationVerdict.SUCCESS; } - public void disconnect() { //noinspection ConstantValue + public void disconnect() + { if (_manager == null) return; @@ -76,12 +90,22 @@ public void disconnect() { //noinspection ConstantValue mcuMgrTransporter.release(); } - public EAndroidDeviceResetterState getState() { + public EAndroidDeviceResetterState getState() + { return _currentState; } private EAndroidDeviceResetterState _currentState = EAndroidDeviceResetterState.NONE; - private void setState(EAndroidDeviceResetterState newState) { + + @Contract(pure = true) + private boolean IsCold() + { + return _currentState == EAndroidDeviceResetterState.NONE + || _currentState == EAndroidDeviceResetterState.COMPLETE; + } + + private void setState(final EAndroidDeviceResetterState newState) + { final EAndroidDeviceResetterState oldState = _currentState; //order _currentState = newState; //order @@ -89,25 +113,55 @@ private void setState(EAndroidDeviceResetterState newState) { stateChangedAdvertisement(oldState, newState); //order } - protected void onCleared() { + protected void onCleared() + { // _manager.setFirmwareUpgradeCallback(null); } private String _lastFatalErrorMessage; - public String getLastFatalErrorMessage() { + @Contract(pure = true) + public String getLastFatalErrorMessage() + { return _lastFatalErrorMessage; } - public void fatalErrorOccurredAdvertisement(String errorMessage) { - _lastFatalErrorMessage = errorMessage; //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates + private void onError(final String errorMessage) + { + onError(errorMessage, null); + } + + private void onError(final String errorMessage, final Exception exception) + { + onErrorImpl(errorMessage, McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(exception)); + } + + private void onError(final String errorMessage, final McuMgrErrorCode exceptionCodeSpecs, final HasReturnCode.GroupReturnCode groupReturnCodeSpecs) + { + onErrorImpl(errorMessage, McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(exceptionCodeSpecs, groupReturnCodeSpecs)); + } + + private void onErrorImpl(final String errorMessage, final int globalErrorCode) + { + setState(EAndroidDeviceResetterState.FAILED); + + fatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); + } + + public void fatalErrorOccurredAdvertisement(final String errorMessage, final int globalErrorCode) + { //this method is meant to be overridden by csharp binding libraries to intercept updates + _lastFatalErrorMessage = errorMessage; } - public void logMessageAdvertisement(String message, String category, String level) { + @Contract(pure = true) + public void logMessageAdvertisement(final String message, final String category, final String level) + { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } - public void stateChangedAdvertisement(EAndroidDeviceResetterState oldState, EAndroidDeviceResetterState currentState) { + @Contract(pure = true) + public void stateChangedAdvertisement(final EAndroidDeviceResetterState oldState, final EAndroidDeviceResetterState currentState) + { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileDownloader.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileDownloader.java index 9e34ddd8..2ac48c31 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileDownloader.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileDownloader.java @@ -3,111 +3,190 @@ import android.bluetooth.BluetoothDevice; import android.content.Context; import androidx.annotation.NonNull; +import io.runtime.mcumgr.McuMgrErrorCode; import io.runtime.mcumgr.McuMgrTransport; import io.runtime.mcumgr.ble.McuMgrBleTransport; import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.managers.FsManager; +import io.runtime.mcumgr.response.HasReturnCode; import io.runtime.mcumgr.transfer.DownloadCallback; import io.runtime.mcumgr.transfer.TransferController; import no.nordicsemi.android.ble.ConnectionPriorityRequest; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @SuppressWarnings("unused") public class AndroidFileDownloader { + private Context _context; + private BluetoothDevice _bluetoothDevice; + private FsManager _fileSystemManager; - @SuppressWarnings("FieldCanBeLocal") - private final McuMgrBleTransport _transport; - private TransferController _controller; + private McuMgrBleTransport _transport; + private TransferController _downloadingController; + private FileDownloaderCallbackProxy _fileDownloaderCallbackProxy; private int _initialBytes; private long _downloadStartTimestamp; private String _remoteFilePathSanitized = ""; private EAndroidFileDownloaderState _currentState = EAndroidFileDownloaderState.NONE; + public AndroidFileDownloader() //this flavour is meant to be used in conjunction with trySetBluetoothDevice() and trySetContext() + { + } + public AndroidFileDownloader(@NonNull final Context context, @NonNull final BluetoothDevice bluetoothDevice) { - _transport = new McuMgrBleTransport(context, bluetoothDevice); + _context = context; + _bluetoothDevice = bluetoothDevice; + } + + public boolean trySetContext(@NonNull final Context context) + { + if (!IsIdleOrCold()) + return false; + + if (!tryInvalidateCachedTransport()) //order + return false; + + _context = context; + return true; } - public EAndroidFileDownloaderVerdict beginDownload(final String remoteFilePath) + public boolean trySetBluetoothDevice(@NonNull final BluetoothDevice bluetoothDevice) { - if (_currentState != EAndroidFileDownloaderState.NONE //if the download is already in progress we bail out - && _currentState != EAndroidFileDownloaderState.ERROR - && _currentState != EAndroidFileDownloaderState.COMPLETE - && _currentState != EAndroidFileDownloaderState.CANCELLED) + if (!IsIdleOrCold()) { - logMessageAdvertisement("Cannot start a new download while another one is still in progress (state=" + _currentState.toString() + ")", "FileDownloader", "ERROR", remoteFilePath); + logMessageAdvertisement("[AFD.TSBD.005] trySetBluetoothDevice() cannot proceed because the uploader is not cold", "FileUploader", "ERROR", _remoteFilePathSanitized); + return false; + } + + if (!tryInvalidateCachedTransport()) //order + { + logMessageAdvertisement("[AFD.TSBD.020] Failed to invalidate the cached-transport instance", "FileUploader", "ERROR", _remoteFilePathSanitized); + return false; + } + + _bluetoothDevice = bluetoothDevice; //order + + logMessageAdvertisement("[AFD.TSBD.030] Successfully set the android-bluetooth-device to the given value", "FileUploader", "TRACE", _remoteFilePathSanitized); + + return true; + } + + public boolean tryInvalidateCachedTransport() + { + if (_transport == null) //already scrapped + return true; + + if (!IsIdleOrCold()) //if the upload is already in progress we bail out + return false; + + disposeFilesystemManager(); // order + disposeTransport(); // order + disposeCallbackProxy(); // order + + return true; + } + + /** + * Initiates a file download asynchronously. The progress is advertised through the callbacks provided by this class. + * Setup interceptors for them to get informed about the status of the firmware-installation. + * + * @param remoteFilePath the remote-file-path to the file on the remote device that you wish to download + * @param initialMtuSize sets the initial MTU for the connection that the McuMgr BLE-transport sets up for the firmware installation that will follow. + * Note that if less than 0 it gets ignored and if it doesn't fall within the range [23, 517] it will cause a hard error. + * @return a verdict indicating whether the file uploading was started successfully or not + */ + public EAndroidFileDownloaderVerdict beginDownload( + final String remoteFilePath, + final int initialMtuSize + // final int windowCapacity, //theoretically nordic firmwares at some point will support this for downloads but as of Q3 2024 there is no support for this + ) + { + if (!IsCold()) //keep first + { + onError("[AFD.BD.000] Another download is already in progress"); return EAndroidFileDownloaderVerdict.FAILED__DOWNLOAD_ALREADY_IN_PROGRESS; } - if (remoteFilePath == null || remoteFilePath.isEmpty()) { - setState(EAndroidFileDownloaderState.ERROR); - fatalErrorOccurredAdvertisement("", "Target-file provided is dud!"); + if (remoteFilePath == null || remoteFilePath.isEmpty()) + { + onError("[AFD.BD.010] Target-file provided is dud!"); return EAndroidFileDownloaderVerdict.FAILED__INVALID_SETTINGS; } - final String remoteFilePathSanitized = remoteFilePath.trim(); - if (remoteFilePathSanitized.endsWith("/")) //the path must point to a file not a directory + _remoteFilePathSanitized = remoteFilePath.trim(); + if (_remoteFilePathSanitized.endsWith("/")) //the path must point to a file not a directory { - setState(EAndroidFileDownloaderState.ERROR); - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, "Provided target-path points to a directory not a file!"); + onError("[AFD.BD.020] Provided target-path points to a directory not a file"); return EAndroidFileDownloaderVerdict.FAILED__INVALID_SETTINGS; } - if (!remoteFilePathSanitized.startsWith("/")) + if (!_remoteFilePathSanitized.startsWith("/")) { - setState(EAndroidFileDownloaderState.ERROR); - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, "Provided target-path is not an absolute path!"); + onError("[AFD.BD.030] Provided target-path is not an absolute path"); return EAndroidFileDownloaderVerdict.FAILED__INVALID_SETTINGS; } - try + if (_context == null) { - _fileSystemManager = new FsManager(_transport); + onError("[AFD.BD.040] No context specified - call trySetContext() first"); + + return EAndroidFileDownloaderVerdict.FAILED__INVALID_SETTINGS; } - catch (final Exception ex) + + if (_bluetoothDevice == null) { - setState(EAndroidFileDownloaderState.ERROR); - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, ex.getMessage()); + onError("[AFD.BD.050] No bluetooth-device specified - call trySetBluetoothDevice() first"); return EAndroidFileDownloaderVerdict.FAILED__INVALID_SETTINGS; } - setLoggingEnabled(false); - requestHighConnectionPriority(); + try + { + resetDownloadState(); //order must be called before ensureTransportIsInitializedExactlyOnce() because the environment might try to set the device via trySetBluetoothDevice()!!! + ensureTransportIsInitializedExactlyOnce(initialMtuSize); //order + setLoggingEnabledOnConnection(false); //order - setState(EAndroidFileDownloaderState.IDLE); - busyStateChangedAdvertisement(true); - fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0); + final EAndroidFileDownloaderVerdict verdict = ensureFilesystemManagerIsInitializedExactlyOnce(); //order + if (verdict != EAndroidFileDownloaderVerdict.SUCCESS) + return verdict; - _initialBytes = 0; + tryEnsureConnectionPriorityOnTransport(); //order + ensureFileDownloaderCallbackProxyIsInitializedExactlyOnce(); //order + + _downloadingController = _fileSystemManager.fileDownload(_remoteFilePathSanitized, _fileDownloaderCallbackProxy); + } + catch (final Exception ex) + { + onError("[AFD.BD.060] Failed to initialize the download operation: " + ex.getMessage(), ex); - _remoteFilePathSanitized = remoteFilePathSanitized; - _controller = _fileSystemManager.fileDownload(remoteFilePathSanitized, new FileDownloaderCallbackProxy()); + return EAndroidFileDownloaderVerdict.FAILED__ERROR_UPON_COMMENCING; + } return EAndroidFileDownloaderVerdict.SUCCESS; } public void pause() { - final TransferController transferController = _controller; + final TransferController transferController = _downloadingController; if (transferController == null) return; setState(EAndroidFileDownloaderState.PAUSED); - setLoggingEnabled(true); + setLoggingEnabledOnConnection(true); transferController.pause(); busyStateChangedAdvertisement(false); } public void resume() { - final TransferController transferController = _controller; + final TransferController transferController = _downloadingController; if (transferController == null) return; @@ -116,11 +195,12 @@ public void resume() busyStateChangedAdvertisement(true); _initialBytes = 0; - setLoggingEnabled(false); + setLoggingEnabledOnConnection(false); transferController.resume(); } - public void disconnect() { + public void disconnect() + { if (_fileSystemManager == null) return; @@ -135,30 +215,110 @@ public void cancel() { setState(EAndroidFileDownloaderState.CANCELLING); //order - final TransferController transferController = _controller; + final TransferController transferController = _downloadingController; if (transferController == null) return; transferController.cancel(); //order } - private void requestHighConnectionPriority() + private void resetDownloadState() { - final McuMgrTransport mcuMgrTransporter = _fileSystemManager.getTransporter(); - if (!(mcuMgrTransporter instanceof McuMgrBleTransport)) + _initialBytes = 0; + _downloadStartTimestamp = 0; + + setState(EAndroidFileDownloaderState.IDLE); + busyStateChangedAdvertisement(true); + fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0); + } + + private void ensureTransportIsInitializedExactlyOnce(int initialMtuSize) + { + if (_transport == null) + { + _transport = new McuMgrBleTransport(_context, _bluetoothDevice); + } + + if (initialMtuSize > 0) + { + _transport.setInitialMtu(initialMtuSize); + } + } + + private void ensureFileDownloaderCallbackProxyIsInitializedExactlyOnce() + { + if (_fileDownloaderCallbackProxy != null) //already initialized return; - final McuMgrBleTransport bleTransporter = (McuMgrBleTransport) mcuMgrTransporter; - bleTransporter.requestConnPriority(ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH); + _fileDownloaderCallbackProxy = new FileDownloaderCallbackProxy(); } - private void setLoggingEnabled(final boolean enabled) + private EAndroidFileDownloaderVerdict ensureFilesystemManagerIsInitializedExactlyOnce() { - final McuMgrTransport mcuMgrTransporter = _fileSystemManager.getTransporter(); - if (!(mcuMgrTransporter instanceof McuMgrBleTransport)) + if (_fileSystemManager != null) //already initialized + return EAndroidFileDownloaderVerdict.SUCCESS; + + try + { + _fileSystemManager = new FsManager(_transport); //order + } + catch (final Exception ex) + { + onError("[AFD.EFMIIEO.010] Failed to initialize the filesystem manager: " + ex.getMessage(), ex); + + return EAndroidFileDownloaderVerdict.FAILED__ERROR_UPON_COMMENCING; + } + + return EAndroidFileDownloaderVerdict.SUCCESS; + } + + private void tryEnsureConnectionPriorityOnTransport() + { + _transport.requestConnPriority(ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH); + } + + private void disposeTransport() + { + if (_transport == null) + return; + + try + { + _transport.disconnect(); + } + catch (Exception ex) + { + // ignore + } + + _transport = null; + } + + private void disposeFilesystemManager() + { + if (_fileSystemManager == null) return; - ((McuMgrBleTransport) mcuMgrTransporter).setLoggingEnabled(enabled); + try + { + _fileSystemManager.closeAll(); + } + catch (McuMgrException e) + { + // ignore + } + + _fileSystemManager = null; + } + + private void disposeCallbackProxy() + { + _fileDownloaderCallbackProxy = null; + } + + private void setLoggingEnabledOnConnection(final boolean enabled) + { + _transport.setLoggingEnabled(enabled); } private void setState(final EAndroidFileDownloaderState newState) @@ -177,56 +337,108 @@ private void setState(final EAndroidFileDownloaderState newState) //00 trivial hotfix to deal with the fact that the filedownload progress% doesnt fill up to 100% } + @Contract(pure = true) + private boolean IsIdleOrCold() + { + return _currentState == EAndroidFileDownloaderState.IDLE || IsCold(); + } + + @Contract(pure = true) + private boolean IsCold() + { + return _currentState == EAndroidFileDownloaderState.NONE + || _currentState == EAndroidFileDownloaderState.ERROR + || _currentState == EAndroidFileDownloaderState.COMPLETE + || _currentState == EAndroidFileDownloaderState.CANCELLED; + } + private String _lastFatalErrorMessage; + @Contract(pure = true) public String getLastFatalErrorMessage() { return _lastFatalErrorMessage; } - public void fatalErrorOccurredAdvertisement(final String resource, final String errorMessage) + private void onError(final String errorMessage) + { + onError(errorMessage, null); + } + + private void onError(final String errorMessage, final Exception exception) + { + onErrorImpl(errorMessage, McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(exception)); + } + + private void onError(final String errorMessage, final McuMgrErrorCode exceptionCodeSpecs, final HasReturnCode.GroupReturnCode groupReturnCodeSpecs) + { + onErrorImpl(errorMessage, McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(exceptionCodeSpecs, groupReturnCodeSpecs)); + } + + private void onErrorImpl(final String errorMessage, final int globalErrorCode) + { + setState(EAndroidFileDownloaderState.ERROR); + + fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, errorMessage, globalErrorCode); + } + + public void fatalErrorOccurredAdvertisement(final String errorMessage, final int globalErrorCode) + { //this method is meant to be overridden by csharp binding libraries to intercept updates + _lastFatalErrorMessage = errorMessage; + } + + public void fatalErrorOccurredAdvertisement( + final String remoteFilePath, + final String errorMessage, + final int globalErrorCode // have a look at EGlobalErrorCode.cs in csharp + ) { - //this method is meant to be overridden by csharp binding libraries to intercept updates _lastFatalErrorMessage = errorMessage; } + @Contract(pure = true) public void busyStateChangedAdvertisement(boolean busyNotIdle) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void cancelledAdvertisement() { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } - //wrapper utility method so that we wont have to constantly pass remoteFilePathSanitized as the first argument currently unused but should be handy in the future + @Contract(pure = true) //wrapper utility method so that we will not have to constantly pass remoteFilePathSanitized as the first argument currently unused but should be handy in the future public void stateChangedAdvertisement(final EAndroidFileDownloaderState oldState, final EAndroidFileDownloaderState newState) { stateChangedAdvertisement(_remoteFilePathSanitized, oldState, newState); } + @Contract(pure = true) public void stateChangedAdvertisement(final String resource, final EAndroidFileDownloaderState oldState, final EAndroidFileDownloaderState newState) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(final int progressPercentage, final float averageThroughput) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void downloadCompletedAdvertisement(final String resource, final byte[] data) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } - //wrapper utility method so that we wont have to constantly pass remoteFilePathSanitized as the fourth argument currently unused but should be handy in the future + @Contract(pure = true) //wrapper utility method so that we will not have to constantly pass remoteFilePathSanitized as the fourth argument currently unused but should be handy in the future private void logMessageAdvertisement(final String message, final String category, final String level) { logMessageAdvertisement(message, category, level, _remoteFilePathSanitized); } + @Contract(pure = true) public void logMessageAdvertisement(final String message, final String category, final String level, final String resource) //wrapper method { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates @@ -263,13 +475,14 @@ public void onDownloadProgressChanged(final int bytesSent, final int fileSize, f } @Override - public void onDownloadFailed(@NonNull final McuMgrException error) + public void onDownloadFailed(@NonNull final McuMgrException exception) { fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0); - setState(EAndroidFileDownloaderState.ERROR); - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, error.getMessage()); - setLoggingEnabled(true); + onError(exception.getMessage(), exception); + setLoggingEnabledOnConnection(true); busyStateChangedAdvertisement(false); + + _downloadingController = null; //game over } @Override @@ -278,8 +491,10 @@ public void onDownloadCanceled() fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0); setState(EAndroidFileDownloaderState.CANCELLED); cancelledAdvertisement(); - setLoggingEnabled(true); + setLoggingEnabledOnConnection(true); busyStateChangedAdvertisement(false); + + _downloadingController = null; //game over } @Override @@ -290,8 +505,10 @@ public void onDownloadCompleted(byte @NotNull [] data) setState(EAndroidFileDownloaderState.COMPLETE); // order vital downloadCompletedAdvertisement(_remoteFilePathSanitized, data); // order vital - setLoggingEnabled(true); + setLoggingEnabledOnConnection(true); busyStateChangedAdvertisement(false); + + _downloadingController = null; //game over } } } diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileUploader.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileUploader.java index 72828f09..f7ce92cf 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileUploader.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFileUploader.java @@ -5,7 +5,6 @@ import androidx.annotation.NonNull; import io.runtime.mcumgr.McuMgrTransport; import io.runtime.mcumgr.ble.McuMgrBleTransport; -import io.runtime.mcumgr.exception.McuMgrErrorException; import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.managers.FsManager; import io.runtime.mcumgr.transfer.FileUploader; @@ -27,10 +26,10 @@ public class AndroidFileUploader private int _initialBytes; private long _uploadStartTimestamp; - private String _remoteFilePathSanitized; + private String _remoteFilePathSanitized = ""; private EAndroidFileUploaderState _currentState = EAndroidFileUploaderState.NONE; - public AndroidFileUploader() + public AndroidFileUploader() //this flavour is meant to be used in conjunction with trySetBluetoothDevice() and trySetContext() { } @@ -42,7 +41,7 @@ public AndroidFileUploader(@NonNull final Context context, @NonNull final Blueto public boolean trySetContext(@NonNull final Context context) { - if (!IsCold()) + if (!IsIdleOrCold()) return false; if (!tryInvalidateCachedTransport()) //order @@ -54,13 +53,22 @@ public boolean trySetContext(@NonNull final Context context) public boolean trySetBluetoothDevice(@NonNull final BluetoothDevice bluetoothDevice) { - if (!IsCold()) + if (!IsIdleOrCold()) + { + logMessageAdvertisement("[AFU.TSBD.005] trySetBluetoothDevice() cannot proceed because the uploader is not cold", "FileUploader", "ERROR", _remoteFilePathSanitized); return false; + } if (!tryInvalidateCachedTransport()) //order + { + logMessageAdvertisement("[AFU.TSBD.020] Failed to invalidate the cached-transport instance", "FileUploader", "ERROR", _remoteFilePathSanitized); return false; + } _bluetoothDevice = bluetoothDevice; //order + + logMessageAdvertisement("[AFU.TSBD.030] Successfully set the android-bluetooth-device to the given value", "FileUploader", "TRACE", _remoteFilePathSanitized); + return true; } @@ -69,87 +77,115 @@ public boolean tryInvalidateCachedTransport() if (_transport == null) //already scrapped return true; - if (!IsCold()) //if the upload is already in progress we bail out + if (!IsIdleOrCold()) //if the upload is already in progress we bail out return false; disposeFilesystemManager(); // order disposeTransport(); // order + disposeCallbackProxy(); // order return true; } - public EAndroidFileUploaderVerdict beginUpload(final String remoteFilePath, final byte[] data) + /** + * Initiates a file upload asynchronously. The progress is advertised through the callbacks provided by this class. + * Setup interceptors for them to get informed about the status of the firmware-installation. + * + * @param remoteFilePath the remote-file-path to save the given data to on the remote device + * @param data the bytes to upload + * @param initialMtuSize sets the initial MTU for the connection that the McuMgr BLE-transport sets up for the firmware installation that will follow. + * Note that if less than 0 it gets ignored and if it doesn't fall within the range [23, 517] it will cause a hard error. + * @param windowCapacity specifies the windows-capacity for the data transfers of the BLE connection - if zero or negative the value provided gets ignored and will be set to 1 by default + * @param memoryAlignment specifies the memory-alignment to use for the data transfers of the BLE connection - if zero or negative the value provided gets ignored and will be set to 1 by default + * @return a verdict indicating whether the file uploading was started successfully or not + */ + public EAndroidFileUploaderVerdict beginUpload( + final String remoteFilePath, + final byte[] data, + final int initialMtuSize, + final int windowCapacity, + final int memoryAlignment + ) { - if (!IsCold()) { - setState(EAndroidFileUploaderState.ERROR); - onError("N/A", "Another upload is already in progress", null); + if (!IsCold()) + { //keep first + onError("Another upload is already in progress"); return EAndroidFileUploaderVerdict.FAILED__OTHER_UPLOAD_ALREADY_IN_PROGRESS; } - if (_context == null) { - setState(EAndroidFileUploaderState.ERROR); - onError("N/A", "No context specified - call trySetContext() first", null); + if (remoteFilePath == null || remoteFilePath.isEmpty()) + { + onError("Provided target-path is empty", null); return EAndroidFileUploaderVerdict.FAILED__INVALID_SETTINGS; } - if (_bluetoothDevice == null) { - setState(EAndroidFileUploaderState.ERROR); - onError("N/A", "No bluetooth-device specified - call trySetBluetoothDevice() first", null); + _remoteFilePathSanitized = remoteFilePath.trim(); + if (_remoteFilePathSanitized.endsWith("/")) //the path must point to a file not a directory + { + onError("Provided target-path points to a directory not a file"); return EAndroidFileUploaderVerdict.FAILED__INVALID_SETTINGS; } - if (remoteFilePath == null || remoteFilePath.isEmpty()) { - setState(EAndroidFileUploaderState.ERROR); - onError("N/A", "Provided target-path is empty", null); + if (!_remoteFilePathSanitized.startsWith("/")) + { + onError("Provided target-path is not an absolute path"); return EAndroidFileUploaderVerdict.FAILED__INVALID_SETTINGS; } - _remoteFilePathSanitized = remoteFilePath.trim(); - if (_remoteFilePathSanitized.endsWith("/")) //the path must point to a file not a directory + if (_context == null) { - setState(EAndroidFileUploaderState.ERROR); - onError(_remoteFilePathSanitized, "Provided target-path points to a directory not a file", null); + onError("No context specified - call trySetContext() first"); return EAndroidFileUploaderVerdict.FAILED__INVALID_SETTINGS; } - if (!_remoteFilePathSanitized.startsWith("/")) + if (_bluetoothDevice == null) { - setState(EAndroidFileUploaderState.ERROR); - onError(_remoteFilePathSanitized, "Provided target-path is not an absolute path", null); + onError("No bluetooth-device specified - call trySetBluetoothDevice() first"); return EAndroidFileUploaderVerdict.FAILED__INVALID_SETTINGS; } - if (data == null) { // data being null is not ok but data.length==0 is perfectly ok because we might want to create empty files - setState(EAndroidFileUploaderState.ERROR); - onError(_remoteFilePathSanitized, "Provided data is null", null); + if (data == null) + { // data being null is not ok but data.length==0 is perfectly ok because we might want to create empty files + onError("Provided data is null"); return EAndroidFileUploaderVerdict.FAILED__INVALID_DATA; } - ensureTransportIsInitializedExactlyOnce(); //order - - final EAndroidFileUploaderVerdict verdict = ensureFilesystemManagerIsInitializedExactlyOnce(); //order - if (verdict != EAndroidFileUploaderVerdict.SUCCESS) - return verdict; - - ensureFileUploaderCallbackProxyIsInitializedExactlyOnce(); //order + try + { + resetUploadState(); //order must be called before ensureTransportIsInitializedExactlyOnce() because the environment might try to set the device via trySetBluetoothDevice()!!! + ensureTransportIsInitializedExactlyOnce(initialMtuSize); //order + setLoggingEnabledOnTransport(false); //order + + final EAndroidFileUploaderVerdict verdict = ensureFilesystemManagerIsInitializedExactlyOnce(); //order + if (verdict != EAndroidFileUploaderVerdict.SUCCESS) + return verdict; + + requestHighConnectionPriorityOnTransport(); //order + ensureFileUploaderCallbackProxyIsInitializedExactlyOnce(); //order + + FileUploader fileUploader = new FileUploader( //00 + _fileSystemManager, + _remoteFilePathSanitized, + data, + Math.max(1, windowCapacity), + Math.max(1, memoryAlignment) + ); - resetUploadState(); //order - setLoggingEnabled(false); + _uploadController = fileUploader.uploadAsync(_fileUploaderCallbackProxy); + } + catch (final Exception ex) + { + onError("Failed to initialize the upload", ex); - _uploadController = new FileUploader( //00 - _fileSystemManager, - _remoteFilePathSanitized, - data, - 3, // window capacity - 4 // memory alignment - ).uploadAsync(_fileUploaderCallbackProxy); + return EAndroidFileUploaderVerdict.FAILED__ERROR_UPON_COMMENCING; + } return EAndroidFileUploaderVerdict.SUCCESS; @@ -157,7 +193,8 @@ public EAndroidFileUploaderVerdict beginUpload(final String remoteFilePath, fina // aka sending multiple packets without waiting for the response } - private void resetUploadState() { + private void resetUploadState() + { _initialBytes = 0; _uploadStartTimestamp = 0; @@ -166,35 +203,41 @@ private void resetUploadState() { fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0); } - private void ensureTransportIsInitializedExactlyOnce() + private void ensureTransportIsInitializedExactlyOnce(int initialMtuSize) { - if (_transport != null) - return; + if (_transport == null) + { + _transport = new McuMgrBleTransport(_context, _bluetoothDevice); + } - _transport = new McuMgrBleTransport(_context, _bluetoothDevice); + if (initialMtuSize > 0) + { + _transport.setInitialMtu(initialMtuSize); + } } - private void ensureFileUploaderCallbackProxyIsInitializedExactlyOnce() { + private void ensureFileUploaderCallbackProxyIsInitializedExactlyOnce() + { if (_fileUploaderCallbackProxy != null) //already initialized return; _fileUploaderCallbackProxy = new FileUploaderCallbackProxy(); } - private EAndroidFileUploaderVerdict ensureFilesystemManagerIsInitializedExactlyOnce() { + private EAndroidFileUploaderVerdict ensureFilesystemManagerIsInitializedExactlyOnce() + { if (_fileSystemManager != null) //already initialized return EAndroidFileUploaderVerdict.SUCCESS; + logMessageAdvertisement("[AFU.EFMIIEO.010] (Re)Initializing filesystem-manager", "FileUploader", "TRACE", _remoteFilePathSanitized); + try { _fileSystemManager = new FsManager(_transport); //order - - requestHighConnectionPriority(_fileSystemManager); //order } catch (final Exception ex) { - setState(EAndroidFileUploaderState.ERROR); - onError(_remoteFilePathSanitized, ex.getMessage(), ex); + onError("[AFU.EFMIIEO.010] Failed to initialize the native file-system-manager", ex); return EAndroidFileUploaderVerdict.FAILED__INVALID_SETTINGS; } @@ -209,7 +252,7 @@ public void pause() return; setState(EAndroidFileUploaderState.PAUSED); - setLoggingEnabled(true); + setLoggingEnabledOnTransport(true); transferController.pause(); busyStateChangedAdvertisement(false); } @@ -225,11 +268,12 @@ public void resume() busyStateChangedAdvertisement(true); _initialBytes = 0; - setLoggingEnabled(false); + setLoggingEnabledOnTransport(false); transferController.resume(); } - public void disconnect() { + public void disconnect() + { if (_fileSystemManager == null) return; @@ -240,25 +284,24 @@ public void disconnect() { mcuMgrTransporter.release(); } - public void cancel() + private String _cancellationReason = ""; + + public void cancel(final String reason) { - setState(EAndroidFileUploaderState.CANCELLING); //order + _cancellationReason = reason; - final TransferController transferController = _uploadController; + cancellingAdvertisement(reason); //order + setState(EAndroidFileUploaderState.CANCELLING); //order + final TransferController transferController = _uploadController; //order if (transferController == null) return; transferController.cancel(); //order } - static private void requestHighConnectionPriority(final FsManager fileSystemManager) + private void requestHighConnectionPriorityOnTransport() { - final McuMgrTransport mcuMgrTransporter = fileSystemManager.getTransporter(); - if (!(mcuMgrTransporter instanceof McuMgrBleTransport)) - return; - - final McuMgrBleTransport bleTransporter = (McuMgrBleTransport) mcuMgrTransporter; - bleTransporter.requestConnPriority(ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH); + _transport.requestConnPriority(ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH); } private void disposeTransport() @@ -266,9 +309,12 @@ private void disposeTransport() if (_transport == null) return; - try { + try + { _transport.disconnect(); - } catch (Exception ex) { + } + catch (Exception ex) + { // ignore } @@ -280,22 +326,26 @@ private void disposeFilesystemManager() if (_fileSystemManager == null) return; - try { + try + { _fileSystemManager.closeAll(); - } catch (McuMgrException e) { + } + catch (McuMgrException e) + { // ignore } _fileSystemManager = null; } - private void setLoggingEnabled(final boolean enabled) + private void disposeCallbackProxy() { - final McuMgrTransport mcuMgrTransporter = _fileSystemManager.getTransporter(); - if (!(mcuMgrTransporter instanceof McuMgrBleTransport)) - return; + _fileUploaderCallbackProxy = null; + } - ((McuMgrBleTransport) mcuMgrTransporter).setLoggingEnabled(enabled); + private void setLoggingEnabledOnTransport(final boolean enabled) + { + _transport.setLoggingEnabled(enabled); } private void setState(final EAndroidFileUploaderState newState) @@ -314,6 +364,12 @@ private void setState(final EAndroidFileUploaderState newState) //00 trivial hotfix to deal with the fact that the file-upload progress% doesn't fill up to 100% } + @Contract(pure = true) + private boolean IsIdleOrCold() + { + return _currentState == EAndroidFileUploaderState.IDLE || IsCold(); + } + @Contract(pure = true) private boolean IsCold() { @@ -325,68 +381,76 @@ private boolean IsCold() private String _lastFatalErrorMessage; + @Contract(pure = true) public String getLastFatalErrorMessage() { return _lastFatalErrorMessage; } - public void onError( - final String remoteFilePath, - final String errorMessage, - final Exception exception - ) + private void onError(final String errorMessage) { - if (!(exception instanceof McuMgrErrorException)) - { - fatalErrorOccurredAdvertisement(remoteFilePath, errorMessage, 0, 0); - return; - } + onError(errorMessage, null); + } + + //@Contract(pure = true) //dont + public void onError(final String errorMessage, final Exception exception) + { + setState(EAndroidFileUploaderState.ERROR); - McuMgrErrorException mcuMgrErrorException = (McuMgrErrorException) exception; fatalErrorOccurredAdvertisement( - remoteFilePath, + _remoteFilePathSanitized, errorMessage, - mcuMgrErrorException.getCode().value(), - (mcuMgrErrorException.getGroupCode() != null ? mcuMgrErrorException.getGroupCode().group : -99) + McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(exception) ); } public void fatalErrorOccurredAdvertisement( final String remoteFilePath, final String errorMessage, - final int mcuMgrErrorCode, // io.runtime.mcumgr.McuMgrErrorCode - final int fsManagerGroupReturnCode // io.runtime.mcumgr.managers.FsManager.ReturnCode + final int globalErrorCode // have a look at EGlobalErrorCode.cs in csharp ) { _lastFatalErrorMessage = errorMessage; //this method is meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void busyStateChangedAdvertisement(final boolean busyNotIdle) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void fileUploadedAdvertisement(final String remoteFilePath) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } - public void cancelledAdvertisement() + @Contract(pure = true) + public void cancellingAdvertisement(final String reason) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) + public void cancelledAdvertisement(final String reason) + { + //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates + } + + @Contract(pure = true) public void stateChangedAdvertisement(final String remoteFilePath, final EAndroidFileUploaderState oldState, final EAndroidFileUploaderState newState) // (final EAndroidFileUploaderState oldState, final EAndroidFileUploaderState newState) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(final int progressPercentage, final float averageThroughput) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } - public void logMessageAdvertisement(final String remoteFilePath, final String message, final String category, final String level) + @Contract(pure = true) + public void logMessageAdvertisement(final String message, final String category, final String level, final String resource) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } @@ -425,9 +489,8 @@ public void onUploadProgressChanged(final int bytesSent, final int fileSize, fin public void onUploadFailed(@NonNull final McuMgrException error) { fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0); - setState(EAndroidFileUploaderState.ERROR); - onError(_remoteFilePathSanitized, error.getMessage(), error); - setLoggingEnabled(true); + onError(error.getMessage(), error); + setLoggingEnabledOnTransport(true); busyStateChangedAdvertisement(false); _uploadController = null; //order @@ -438,8 +501,8 @@ public void onUploadCanceled() { fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0); setState(EAndroidFileUploaderState.CANCELLED); - cancelledAdvertisement(); - setLoggingEnabled(true); + cancelledAdvertisement(_cancellationReason); + setLoggingEnabledOnTransport(true); busyStateChangedAdvertisement(false); _uploadController = null; //order @@ -451,7 +514,7 @@ public void onUploadCompleted() //fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(100, 0); //no need this is taken care of inside setState() setState(EAndroidFileUploaderState.COMPLETE); fileUploadedAdvertisement(_remoteFilePathSanitized); - setLoggingEnabled(true); + setLoggingEnabledOnTransport(true); busyStateChangedAdvertisement(false); _uploadController = null; //order diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareEraser.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareEraser.java index 10f7336e..4661c27a 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareEraser.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareEraser.java @@ -4,15 +4,19 @@ import android.content.Context; import androidx.annotation.NonNull; import io.runtime.mcumgr.McuMgrCallback; +import io.runtime.mcumgr.McuMgrErrorCode; import io.runtime.mcumgr.McuMgrTransport; import io.runtime.mcumgr.ble.McuMgrBleTransport; import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.managers.ImageManager; +import io.runtime.mcumgr.response.HasReturnCode; import io.runtime.mcumgr.response.img.McuMgrImageResponse; import io.runtime.mcumgr.response.img.McuMgrImageStateResponse; +import org.jetbrains.annotations.Contract; @SuppressWarnings("unused") -public class AndroidFirmwareEraser { +public class AndroidFirmwareEraser +{ private ImageManager _imageManager; private final McuMgrBleTransport _transport; @@ -23,43 +27,63 @@ public class AndroidFirmwareEraser { * @param context the android-context of the calling environment * @param bluetoothDevice the device to perform the firmware-install on */ - public AndroidFirmwareEraser(@NonNull final Context context, @NonNull final BluetoothDevice bluetoothDevice) { + public AndroidFirmwareEraser(@NonNull final Context context, @NonNull final BluetoothDevice bluetoothDevice) + { _transport = new McuMgrBleTransport(context, bluetoothDevice); } - public void beginErasure(final int imageIndex) { - busyStateChangedAdvertisement(true); - - setState(EAndroidFirmwareEraserState.ERASING); - - _imageManager = new ImageManager(_transport); - _imageManager.erase(imageIndex, new McuMgrCallback() { - @Override - public void onResponse(@NonNull final McuMgrImageResponse response) { - if (!response.isSuccess()) { // check for an error return code - fatalErrorOccurredAdvertisement("Erasure failed (error-code '" + response.getReturnCode().toString() + "')"); - - setState(EAndroidFirmwareEraserState.FAILED); - return; + public EAndroidFirmwareEraserInitializationVerdict beginErasure(final int imageIndex) + { + if (!IsCold()) + { //keep first + onError("[AFE.BE.000] Another reset operation is already in progress (state='" + _currentState + "')"); + + return EAndroidFirmwareEraserInitializationVerdict.FAILED__OTHER_ERASURE_ALREADY_IN_PROGRESS; + } + + try + { + setState(EAndroidFirmwareEraserState.IDLE); //order + _imageManager = new ImageManager(_transport); //order + busyStateChangedAdvertisement(true); //order + setState(EAndroidFirmwareEraserState.ERASING); //order + + AndroidFirmwareEraser self = this; + _imageManager.erase(imageIndex, new McuMgrCallback() + { + @Override + public void onResponse(@NonNull final McuMgrImageResponse response) + { + if (!response.isSuccess()) + { // check for an error return code + self.onError("[AFE.BE.010] Erasure failed (error-code '" + response.getReturnCode().toString() + "')", response.getReturnCode(), response.getGroupReturnCode()); + return; + } + + readImageErasure(); + setState(EAndroidFirmwareEraserState.COMPLETE); } - readImageErasure(); - - setState(EAndroidFirmwareEraserState.COMPLETE); - } + @Override + public void onError(@NonNull final McuMgrException exception) + { + self.onError("[AFE.BE.020] Erasure failed '" + exception.getMessage() + "'", exception); - @Override - public void onError(@NonNull final McuMgrException error) { - fatalErrorOccurredAdvertisement("Erasure failed '" + error.getMessage() + "'"); - - busyStateChangedAdvertisement(false); - - setState(EAndroidFirmwareEraserState.IDLE); - } - }); + busyStateChangedAdvertisement(false); + } + }); + } + catch (final Exception ex) + { + onError("[AFE.BE.010] Failed to initialize erase operation: '" + ex.getMessage() + "'", ex); + return EAndroidFirmwareEraserInitializationVerdict.FAILED__ERROR_UPON_COMMENCING; + } + + return EAndroidFirmwareEraserInitializationVerdict.SUCCESS; } - public void disconnect() { + public void disconnect() + { if (_imageManager == null) return; @@ -70,8 +94,17 @@ public void disconnect() { mcuMgrTransporter.release(); } + @Contract(pure = true) + private boolean IsCold() + { + return _currentState == EAndroidFirmwareEraserState.NONE + || _currentState == EAndroidFirmwareEraserState.COMPLETE; + } + private EAndroidFirmwareEraserState _currentState = EAndroidFirmwareEraserState.NONE; - private void setState(EAndroidFirmwareEraserState newState) { + + private void setState(EAndroidFirmwareEraserState newState) + { final EAndroidFirmwareEraserState oldState = _currentState; //order _currentState = newState; //order @@ -79,42 +112,78 @@ private void setState(EAndroidFirmwareEraserState newState) { stateChangedAdvertisement(oldState, newState); //order } - public void stateChangedAdvertisement(EAndroidFirmwareEraserState oldState, EAndroidFirmwareEraserState currentState) { + @Contract(pure = true) + public void stateChangedAdvertisement(EAndroidFirmwareEraserState oldState, EAndroidFirmwareEraserState currentState) + { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } private String _lastFatalErrorMessage; - public String getLastFatalErrorMessage() { + @Contract(pure = true) + public String getLastFatalErrorMessage() + { return _lastFatalErrorMessage; } - public void fatalErrorOccurredAdvertisement(String errorMessage) { + private void onError(final String errorMessage) + { + onError(errorMessage, null); + } + + private void onError(final String errorMessage, final Exception exception) + { + onErrorImpl(errorMessage, McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(exception)); + } + + private void onError(final String errorMessage, final McuMgrErrorCode exceptionCodeSpecs, final HasReturnCode.GroupReturnCode groupReturnCodeSpecs) + { + onErrorImpl(errorMessage, McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(exceptionCodeSpecs, groupReturnCodeSpecs)); + } + + private void onErrorImpl(final String errorMessage, final int globalErrorCode) + { + setState(EAndroidFirmwareEraserState.FAILED); + + fatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); + } + + public void fatalErrorOccurredAdvertisement(final String errorMessage, int globalErrorCode) + { _lastFatalErrorMessage = errorMessage; //this method is meant to be overridden by csharp binding libraries to intercept updates } - public void logMessageAdvertisement(String message, String category, String level) { + @Contract(pure = true) + public void logMessageAdvertisement(final String message, final String category, final String level) + { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } - public void busyStateChangedAdvertisement(boolean busyNotIdle) { + @Contract(pure = true) + public void busyStateChangedAdvertisement(final boolean busyNotIdle) + { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } - private void readImageErasure() { + private void readImageErasure() + { busyStateChangedAdvertisement(true); - _imageManager.list(new McuMgrCallback() { + AndroidFirmwareEraser self = this; + _imageManager.list(new McuMgrCallback() + { @Override - public void onResponse(@NonNull final McuMgrImageStateResponse response) { + public void onResponse(@NonNull final McuMgrImageStateResponse response) + { // postReady(response); busyStateChangedAdvertisement(false); } @Override - public void onError(@NonNull final McuMgrException error) { - fatalErrorOccurredAdvertisement(error.getMessage()); + public void onError(@NonNull final McuMgrException exception) + { + self.onError("[AFE.RIE.OE.010] Failed to read firmware images after firmware erasure : " + exception.getMessage(), exception); busyStateChangedAdvertisement(false); } }); diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareInstaller.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareInstaller.java index 792a7523..a9199f4a 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareInstaller.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/AndroidFirmwareInstaller.java @@ -11,11 +11,15 @@ import io.runtime.mcumgr.dfu.FirmwareUpgradeCallback; import io.runtime.mcumgr.dfu.FirmwareUpgradeController; import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager; +import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager.Settings; +import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager.Settings.Builder; import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager.State; import io.runtime.mcumgr.dfu.mcuboot.model.ImageSet; import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.exception.McuMgrTimeoutException; import no.nordicsemi.android.ble.ConnectionPriorityRequest; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; @SuppressWarnings("unused") public class AndroidFirmwareInstaller @@ -47,9 +51,24 @@ public AndroidFirmwareInstaller(@NonNull final Context context, @NonNull final B _transport = new McuMgrBleTransport(context, bluetoothDevice); } + /** + * Initiates a firmware installation asynchronously. The progress is advertised through the callbacks provided by this class. + * Setup interceptors for them to get informed about the status of the firmware-installation. + * + * @param data the firmware bytes to install - can also be a zipped byte stream + * @param mode the mode of the installation - typically you want to set this to TEST_AND_CONFIRM in production environments + * @param initialMtuSize sets the initial MTU for the connection that the McuMgr BLE-transport sets up for the firmware installation that will follow. + * Note that if less than 0 it gets ignored and if it doesn't fall within the range [23, 517] it will cause a hard error. + * @param eraseSettings specifies whether the previous settings should be erased on the target-device + * @param estimatedSwapTimeInMilliseconds specifies the amount of time to wait before probing the device to see if the firmware that got installed managed to reboot the device successfully - if negative the setting gets ignored + * @param windowCapacity specifies the windows-capacity for the data transfers of the BLE connection - if zero or negative the value provided gets ignored and will be set to 1 by default + * @param memoryAlignment specifies the memory-alignment to use for the data transfers of the BLE connection - if zero or negative the value provided gets ignored and will be set to 1 by default + * @return a verdict indicating whether the firmware installation was started successfully or not + */ public EAndroidFirmwareInstallationVerdict beginInstallation( @NonNull final byte[] data, @NonNull final EAndroidFirmwareInstallationMode mode, + final int initialMtuSize, final boolean eraseSettings, final int estimatedSwapTimeInMilliseconds, final int windowCapacity, @@ -61,24 +80,9 @@ public EAndroidFirmwareInstallationVerdict beginInstallation( && _currentState != EAndroidFirmwareInstallationState.COMPLETE && _currentState != EAndroidFirmwareInstallationState.CANCELLED) { - return EAndroidFirmwareInstallationVerdict.FAILED__INSTALLATION_ALREADY_IN_PROGRESS; - } - - _manager = new FirmwareUpgradeManager(_transport); - _manager.setFirmwareUpgradeCallback(new FirmwareInstallCallbackProxy()); - - _handlerThread = new HandlerThread("AndroidFirmwareInstaller.HandlerThread"); //todo peer review whether this is the best way to go maybe we should be getting this from the call environment? - _handlerThread.start(); + onError(EAndroidFirmwareInstallerFatalErrorType.FAILED__INSTALLATION_ALREADY_IN_PROGRESS, "[AFI.BI.000] Another firmware installation is already in progress"); - _handler = new Handler(_handlerThread.getLooper()); - - if (estimatedSwapTimeInMilliseconds >= 0 && estimatedSwapTimeInMilliseconds <= 1000) - { //it is better to just warn the calling environment instead of erroring out - logMessageAdvertisement( - "Estimated swap-time of '" + estimatedSwapTimeInMilliseconds + "' milliseconds seems suspiciously low - did you mean to say '" + (estimatedSwapTimeInMilliseconds * 1000) + "' milliseconds?", - "FirmwareInstaller", - "WARN" - ); + return EAndroidFirmwareInstallationVerdict.FAILED__INSTALLATION_ALREADY_IN_PROGRESS; } ImageSet images = new ImageSet(); @@ -94,39 +98,45 @@ public EAndroidFirmwareInstallationVerdict beginInstallation( } catch (final Exception ex2) { - emitFatalError(EAndroidFirmwareInstallerFatalErrorType.INVALID_FIRMWARE, ex2.getMessage()); + onError(EAndroidFirmwareInstallerFatalErrorType.INVALID_FIRMWARE, ex2.getMessage(), ex2); return EAndroidFirmwareInstallationVerdict.FAILED__INVALID_DATA_FILE; } } - FirmwareUpgradeManager.Settings.Builder settingsBuilder = new FirmwareUpgradeManager.Settings.Builder(); - try - { - requestHighConnectionPriority(); - - _manager.setMode(mode.getValueFirmwareUpgradeManagerMode()); //0 + _manager = new FirmwareUpgradeManager(_transport); + _manager.setFirmwareUpgradeCallback(new FirmwareInstallCallbackProxy()); - if (estimatedSwapTimeInMilliseconds >= 0) - { - settingsBuilder.setEstimatedSwapTime(estimatedSwapTimeInMilliseconds); //1 - } + _handlerThread = new HandlerThread("AndroidFirmwareInstaller.HandlerThread"); //todo peer review whether this is the best way to go maybe we should be getting this from the call environment? + _handlerThread.start(); - if (windowCapacity >= 0) - { - settingsBuilder.setWindowCapacity(windowCapacity); //2 - } + _handler = new Handler(_handlerThread.getLooper()); - if (memoryAlignment >= 1) - { - settingsBuilder.setMemoryAlignment(memoryAlignment); //3 - } + if (estimatedSwapTimeInMilliseconds >= 0 && estimatedSwapTimeInMilliseconds <= 1000) + { //it is better to just warn the calling environment instead of erroring out + logMessageAdvertisement( + "Estimated swap-time of '" + estimatedSwapTimeInMilliseconds + "' milliseconds seems suspiciously low - did you mean to say '" + (estimatedSwapTimeInMilliseconds * 1000) + "' milliseconds?", + "FirmwareInstaller", + "WARN" + ); + } - settingsBuilder.setEraseAppSettings(eraseSettings); + @NotNull Settings settings; + try + { + configureConnectionSettings(initialMtuSize); + + settings = digestFirmwareInstallationManagerSettings( + mode, + eraseSettings, + estimatedSwapTimeInMilliseconds, + windowCapacity, + memoryAlignment + ); } catch (final Exception ex) { - emitFatalError(EAndroidFirmwareInstallerFatalErrorType.INVALID_SETTINGS, ex.getMessage()); + onError(EAndroidFirmwareInstallerFatalErrorType.INVALID_SETTINGS, ex.getMessage(), ex); return EAndroidFirmwareInstallationVerdict.FAILED__INVALID_SETTINGS; } @@ -135,16 +145,43 @@ public EAndroidFirmwareInstallationVerdict beginInstallation( { setState(EAndroidFirmwareInstallationState.IDLE); - _manager.start(images, settingsBuilder.build()); + _manager.start(images, settings); } catch (final Exception ex) { - emitFatalError(EAndroidFirmwareInstallerFatalErrorType.DEPLOYMENT_FAILED, ex.getMessage()); + onError(EAndroidFirmwareInstallerFatalErrorType.DEPLOYMENT_FAILED, ex.getMessage(), ex); return EAndroidFirmwareInstallationVerdict.FAILED__DEPLOYMENT_ERROR; } return EAndroidFirmwareInstallationVerdict.SUCCESS; + } + + private @NotNull Settings digestFirmwareInstallationManagerSettings(@NotNull EAndroidFirmwareInstallationMode mode, boolean eraseSettings, int estimatedSwapTimeInMilliseconds, int windowCapacity, int memoryAlignment) + { + Builder settingsBuilder = new FirmwareUpgradeManager.Settings.Builder(); + + _manager.setMode(mode.getValueFirmwareUpgradeManagerMode()); //0 + + if (estimatedSwapTimeInMilliseconds >= 0) + { + settingsBuilder.setEstimatedSwapTime(estimatedSwapTimeInMilliseconds); //1 + } + + if (windowCapacity >= 2) + { + settingsBuilder.setWindowCapacity(windowCapacity); //2 + } + + if (memoryAlignment >= 2) + { + settingsBuilder.setMemoryAlignment(memoryAlignment); //3 + } + + settingsBuilder.setEraseAppSettings(eraseSettings); + + return settingsBuilder.build(); + //0 set the installation mode // @@ -166,7 +203,8 @@ public EAndroidFirmwareInstallationVerdict beginInstallation( //3 Set the selected memory alignment. In the app this defaults to 4 to match Nordic devices, but can be modified in the UI. } - public void disconnect() { + public void disconnect() + { if (_manager == null) return; @@ -202,47 +240,62 @@ protected void onCleared() private String _lastFatalErrorMessage; + @Contract(pure = true) public String getLastFatalErrorMessage() { return _lastFatalErrorMessage; } - public void emitFatalError(EAndroidFirmwareInstallerFatalErrorType fatalErrorType, final String errorMessage) + public void onError(final EAndroidFirmwareInstallerFatalErrorType fatalErrorType, final String errorMessage) { - EAndroidFirmwareInstallationState currentStateSnapshot = _currentState; //00 + onError(fatalErrorType, errorMessage, null); + } - setState(EAndroidFirmwareInstallationState.ERROR); // order - fatalErrorOccurredAdvertisement(currentStateSnapshot, fatalErrorType, errorMessage); // order + public void onError(final EAndroidFirmwareInstallerFatalErrorType fatalErrorType, final String errorMessage, final Exception ex) + { + EAndroidFirmwareInstallationState currentStateSnapshot = _currentState; //00 order + setState(EAndroidFirmwareInstallationState.ERROR); // order + fatalErrorOccurredAdvertisement( // order + currentStateSnapshot, + fatalErrorType, + errorMessage, + McuMgrExceptionHelpers.DeduceGlobalErrorCodeFromException(ex) + ); //00 we want to let the calling environment know in which exact state the fatal error happened in } - public void fatalErrorOccurredAdvertisement(final EAndroidFirmwareInstallationState state, final EAndroidFirmwareInstallerFatalErrorType fatalErrorType, final String errorMessage) + //this method is meant to be overridden by csharp binding libraries to intercept updates + public void fatalErrorOccurredAdvertisement(final EAndroidFirmwareInstallationState state, final EAndroidFirmwareInstallerFatalErrorType fatalErrorType, final String errorMessage, final int globalErrorCode) { - //this method is meant to be overridden by csharp binding libraries to intercept updates _lastFatalErrorMessage = errorMessage; } + @Contract(pure = true) public void logMessageAdvertisement(final String message, final String category, final String level) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void cancelledAdvertisement() { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void busyStateChangedAdvertisement(final boolean busyNotIdle) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void stateChangedAdvertisement(final EAndroidFirmwareInstallationState oldState, final EAndroidFirmwareInstallationState currentState) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates } + @Contract(pure = true) public void firmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(final int progressPercentage, final float averageThroughput) { //this method is intentionally empty its meant to be overridden by csharp binding libraries to intercept updates @@ -354,7 +407,7 @@ else if (state == State.CONFIRM && ex instanceof McuMgrTimeoutException) fatalErrorType = EAndroidFirmwareInstallerFatalErrorType.FIRMWARE_IMAGE_SWAP_TIMEOUT; } - emitFatalError(fatalErrorType, ex.getMessage()); + onError(fatalErrorType, ex.getMessage(), ex); setLoggingEnabled(true); // Timber.e(error, "Install failed"); busyStateChangedAdvertisement(false); @@ -417,13 +470,19 @@ public void onUploadProgressChanged(final int bytesSent, final int imageSize, fi } } - private void requestHighConnectionPriority() + private void configureConnectionSettings(int initialMtuSize) { final McuMgrTransport transporter = _manager.getTransporter(); if (!(transporter instanceof McuMgrBleTransport)) return; final McuMgrBleTransport bleTransporter = (McuMgrBleTransport) transporter; + + if (initialMtuSize > 0) + { + bleTransporter.setInitialMtu(initialMtuSize); + } + bleTransporter.requestConnPriority(ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH); } diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidDeviceResetterInitializationVerdict.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidDeviceResetterInitializationVerdict.java new file mode 100644 index 00000000..fdf6a23e --- /dev/null +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidDeviceResetterInitializationVerdict.java @@ -0,0 +1,17 @@ +package no.laerdal.mcumgr_laerdal_wrapper; + +public enum EAndroidDeviceResetterInitializationVerdict +{ + SUCCESS(0), + FAILED__ERROR_UPON_COMMENCING(1), //connection problems + FAILED__OTHER_RESET_ALREADY_IN_PROGRESS(2); + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final int _value; + + EAndroidDeviceResetterInitializationVerdict(int value) + { + _value = value; + } +} + diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidDeviceResetterState.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidDeviceResetterState.java index fe0aac3f..0035cf46 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidDeviceResetterState.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidDeviceResetterState.java @@ -1,6 +1,7 @@ package no.laerdal.mcumgr_laerdal_wrapper; -public enum EAndroidDeviceResetterState { +public enum EAndroidDeviceResetterState +{ NONE(0), IDLE(1), RESETTING(2), diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileDownloaderVerdict.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileDownloaderVerdict.java index 69256fa3..423e388a 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileDownloaderVerdict.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileDownloaderVerdict.java @@ -4,7 +4,8 @@ public enum EAndroidFileDownloaderVerdict //this must mirror the enum values of { SUCCESS(0), FAILED__INVALID_SETTINGS(1), - FAILED__DOWNLOAD_ALREADY_IN_PROGRESS(2); + FAILED__ERROR_UPON_COMMENCING(2), + FAILED__DOWNLOAD_ALREADY_IN_PROGRESS(3); @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final int _value; diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileUploaderVerdict.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileUploaderVerdict.java index 9428b8ad..674e4bc5 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileUploaderVerdict.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFileUploaderVerdict.java @@ -3,9 +3,10 @@ public enum EAndroidFileUploaderVerdict //this must mirror the enum values of E[Android|iOS]FileUploaderVerdict { SUCCESS(0), - FAILED__INVALID_SETTINGS(1), - FAILED__INVALID_DATA(2), - FAILED__OTHER_UPLOAD_ALREADY_IN_PROGRESS(3); + FAILED__INVALID_DATA(1), + FAILED__INVALID_SETTINGS(2), + FAILED__ERROR_UPON_COMMENCING(3), //connection problems + FAILED__OTHER_UPLOAD_ALREADY_IN_PROGRESS(4); @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final int _value; diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareEraserInitializationVerdict.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareEraserInitializationVerdict.java new file mode 100644 index 00000000..e155a166 --- /dev/null +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareEraserInitializationVerdict.java @@ -0,0 +1,16 @@ +package no.laerdal.mcumgr_laerdal_wrapper; + +public enum EAndroidFirmwareEraserInitializationVerdict +{ + SUCCESS(0), + FAILED__ERROR_UPON_COMMENCING(1), //connection problems + FAILED__OTHER_ERASURE_ALREADY_IN_PROGRESS(2); + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final int _value; + + EAndroidFirmwareEraserInitializationVerdict(int value) + { + _value = value; + } +} diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareEraserState.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareEraserState.java index ae4b31e8..48f1f055 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareEraserState.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareEraserState.java @@ -1,6 +1,7 @@ package no.laerdal.mcumgr_laerdal_wrapper; -public enum EAndroidFirmwareEraserState { +public enum EAndroidFirmwareEraserState +{ NONE(0), IDLE(1), ERASING(2), diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationMode.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationMode.java index 33986d6f..f2def48b 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationMode.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationMode.java @@ -39,11 +39,13 @@ public enum EAndroidFirmwareInstallationMode @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final int _value; - EAndroidFirmwareInstallationMode(int value) { + EAndroidFirmwareInstallationMode(int value) + { _value = value; } - FirmwareUpgradeManager.Mode getValueFirmwareUpgradeManagerMode() throws RuntimeException { + FirmwareUpgradeManager.Mode getValueFirmwareUpgradeManagerMode() throws RuntimeException + { switch (_value) { case 0: diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationVerdict.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationVerdict.java index 8dc78c0d..29172194 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationVerdict.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallationVerdict.java @@ -11,9 +11,8 @@ public enum EAndroidFirmwareInstallationVerdict @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final int _value; - EAndroidFirmwareInstallationVerdict(int value) { + EAndroidFirmwareInstallationVerdict(int value) + { _value = value; } } - - diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallerFatalErrorType.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallerFatalErrorType.java index e7216131..580bca21 100644 --- a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallerFatalErrorType.java +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/EAndroidFirmwareInstallerFatalErrorType.java @@ -7,7 +7,8 @@ public enum EAndroidFirmwareInstallerFatalErrorType //this must mirror the enum INVALID_FIRMWARE(2), DEPLOYMENT_FAILED(3), FIRMWARE_IMAGE_SWAP_TIMEOUT(4), - FIRMWARE_UPLOADING_ERRORED_OUT(5); + FIRMWARE_UPLOADING_ERRORED_OUT(5), + FAILED__INSTALLATION_ALREADY_IN_PROGRESS(6); @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final int _value; diff --git a/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/McuMgrExceptionHelpers.java b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/McuMgrExceptionHelpers.java new file mode 100644 index 00000000..5cf37370 --- /dev/null +++ b/Laerdal.McuMgr.Bindings.Android.Native/mcumgr-laerdal-wrapper/src/main/java/no/laerdal/mcumgr_laerdal_wrapper/McuMgrExceptionHelpers.java @@ -0,0 +1,34 @@ +package no.laerdal.mcumgr_laerdal_wrapper; + +import io.runtime.mcumgr.McuMgrErrorCode; +import io.runtime.mcumgr.exception.McuMgrErrorException; +import io.runtime.mcumgr.response.HasReturnCode; + +final class McuMgrExceptionHelpers +{ + // this method must be kept aligned between our ios lib and our android lib + public static int DeduceGlobalErrorCodeFromException(final Exception exception) + { + if (!(exception instanceof McuMgrErrorException)) + return -99; + + McuMgrErrorException mcuMgrErrorException = (McuMgrErrorException) exception; + McuMgrErrorCode exceptionCodeSpecs = mcuMgrErrorException.getCode(); + HasReturnCode.GroupReturnCode groupReturnCodeSpecs = mcuMgrErrorException.getGroupCode(); + + return DeduceGlobalErrorCodeFromException(exceptionCodeSpecs, groupReturnCodeSpecs); + } + + public static int DeduceGlobalErrorCodeFromException(final McuMgrErrorCode exceptionCodeSpecs, final HasReturnCode.GroupReturnCode groupReturnCodeSpecs) + { + return groupReturnCodeSpecs == null + ? exceptionCodeSpecs.value() // 00 + : (((groupReturnCodeSpecs.group + 1) * 1000) + groupReturnCodeSpecs.rc); // 10 + + //00 for auth errors and for nordic devices that do not support smp v2 these error codes occupy the range [0,999] + // + //10 for more evolved errors on nordic devices that do support smp v2 these error codes get mapped to values 1000+ + // + // in this way all error codes get mapped to a single human-readable enum in csharp called EGlobalErrorCode + } +} diff --git a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NativeBuilder.targets b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NativeBuilder.targets index 3fc3d8a4..b8eacda9 100644 --- a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NativeBuilder.targets +++ b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NativeBuilder.targets @@ -15,7 +15,10 @@ - + @@ -23,11 +26,11 @@ gradle - /usr/local/opt/openjdk@17 - /opt/homebrew/opt/openjdk@17 + /usr/local/opt/openjdk@17 + /opt/homebrew/opt/openjdk@17 - C:\\Program Files\\OpenJDK\\jdk-17.0.2 + C:\\Program Files\\OpenJDK\\jdk-17.0.2 $(JAVA_HOME) @@ -50,7 +53,7 @@ - + $(ANDROID_HOME) - /Users/$(USERNAME)/Library/Android/sdk - /home/$(USERNAME)/Android/Sdk - C:\\Users\\$(USERNAME)\\AppData\\Local\\Android\\sdk + /Users/$(USERNAME)/Library/Android/sdk + /home/$(USERNAME)/Android/Sdk + C:\\Users\\$(USERNAME)\\AppData\\Local\\Android\\sdk $(McuMgrLaerdalWrapperLibSourceDirectory)/mcumgr-laerdal-wrapper/build/outputs/aar/mcumgr-laerdal-wrapper-release.aar @@ -76,15 +79,16 @@ - + - - + @@ -94,10 +98,10 @@ - - - - + + + + @@ -105,19 +109,19 @@ - - - + + + - - - + + + - - - + + + diff --git a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NetX.targets b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NetX.targets index 97342259..98969220 100644 --- a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NetX.targets +++ b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.NetX.targets @@ -2,14 +2,6 @@ - - 10.0 - - - 34.0 - 34 - - @@ -45,9 +37,9 @@ - - - + + + diff --git a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj index 85ea2c65..35d1227b 100644 --- a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj +++ b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj @@ -1,10 +1,12 @@  - true - true - true - true + true + true + true + true + 10.0 + @@ -16,8 +18,13 @@ true - true + true + + $(Laerdal_Bindings_Android___DotnetTargetPlatformVersion) + 34 + 21 + pdbonly $(DefineConstants);TRACE true @@ -55,10 +62,10 @@ true - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 $(PackageId) $(Authors) @@ -104,6 +111,9 @@ + + + diff --git a/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj b/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj index 4540d8fb..0569bbac 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj +++ b/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj @@ -2,27 +2,31 @@ - $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)) ) - $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)) ) - $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)) ) - true + $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)) ) + $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)) ) + $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)) ) + true - net8.0-maccatalyst + net8.0-maccatalyst - netstandard2.1 + netstandard2.1 true - true - true + true + true + true - true + true - 13.1 + + $(Laerdal_Bindings_MacCatalyst___DotnetTargetPlatformVersion) + 17.0 + 13.1 bin\ Library @@ -35,7 +39,7 @@ $(NativeFrameworkParentFolderpath)/McuMgrBindingsiOS.framework - + @@ -69,10 +73,10 @@ $(AllowedReferenceRelatedFileExtensions);.pdb - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 $(PackageId) McuMgr Bindings for MacCatalyst - MAUI ready @@ -118,27 +122,29 @@ -v -v -v -v - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -160,10 +166,11 @@ BeforeTargets="PrepareForBuild" Condition=" '$(ShouldBuildNativeLibraries)' == 'true' and '$(DesignTimeBuild)' != 'true' and '$(BuildingProject)' == 'true' "> - + + <_CliCommand>$(_CliCommand) SUPPORTS_MACCATALYST='YES' <_CliCommand>$(_CliCommand) SWIFT_OUTPUT_PATH='$(NativeFrameworkParentFolderpath)' <_CliCommand>$(_CliCommand) XCODE_IDE_DEV_PATH='$(Laerdal_Bindings_MacCatalyst___Xcode_Ide_Dev_Path)' <_CliCommand>$(_CliCommand) XCODEBUILD_TARGET_SDK='macosx' @@ -217,7 +224,7 @@ - + diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/Laerdal.Mac.CompileAndGenerateFatLibs.sh b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/Laerdal.Mac.CompileAndGenerateFatLibs.sh index 4bd77773..88c6ecb3 100755 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/Laerdal.Mac.CompileAndGenerateFatLibs.sh +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/Laerdal.Mac.CompileAndGenerateFatLibs.sh @@ -35,7 +35,7 @@ declare SWIFT_PACKAGES_PATH="./packages" declare OUTPUT_FOLDER_POSTFIX="" if [ "${XCODEBUILD_TARGET_SDK}" == "macosx" ]; then - OUTPUT_FOLDER_POSTFIX="" # special case for mac catalyst sdk 15.2 wanted this to be "-maccatalyst" but sdk 15.4 wants it to be empty go figure ... + OUTPUT_FOLDER_POSTFIX="-maccatalyst" # special case for mac catalyst else OUTPUT_FOLDER_POSTFIX="-${XCODEBUILD_TARGET_SDK}" fi diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS.xcodeproj/project.pbxproj b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS.xcodeproj/project.pbxproj index a2e44903..63a1048c 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS.xcodeproj/project.pbxproj +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS.xcodeproj/project.pbxproj @@ -8,13 +8,16 @@ /* Begin PBXBuildFile section */ B028FB802940DF1400CB71EB /* iOSMcuManagerLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = B028FB7F2940DF1400CB71EB /* iOSMcuManagerLibrary */; }; + B04DC3652CD3C7810048B553 /* EIOSDeviceResetInitializationVerdict.swift in Sources */ = {isa = PBXBuildFile; fileRef = B04DC3642CD3C7810048B553 /* EIOSDeviceResetInitializationVerdict.swift */; }; + B04DC3672CD3E1A20048B553 /* EIOSFirmwareErasureInitializationVerdict.swift in Sources */ = {isa = PBXBuildFile; fileRef = B04DC3662CD3E1A20048B553 /* EIOSFirmwareErasureInitializationVerdict.swift */; }; + B0FF8AE92CCFF8E1004B39DE /* IOSListenerForFileUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0FF8AE82CCFF8E1004B39DE /* IOSListenerForFileUploader.swift */; }; E769D0FFF3A39C575B982F6D /* InvalidFirmwareInstallationModeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769DB7090742CC7D6AFD01D /* InvalidFirmwareInstallationModeError.swift */; }; E769D1175ADD137DCA6B0207 /* EIOSFirmwareEraserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769D3130BA737A0C282D1DD /* EIOSFirmwareEraserState.swift */; }; E769D15459466B0C5BA19E21 /* EIOSFirmwareInstallationVerdict.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769D1DEDAD5F89C361DF28C /* EIOSFirmwareInstallationVerdict.swift */; }; E769D165ADFDDBED8F709DAE /* EIOSFirmwareInstallationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769DC8A37490B096716C6B8 /* EIOSFirmwareInstallationMode.swift */; }; E769D26DF00FF487503F9A77 /* McuMgrBindingsiOS.h in Headers */ = {isa = PBXBuildFile; fileRef = E769DC768A6D6F0D1CAC3C06 /* McuMgrBindingsiOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; E769D33171D78172F096C8A4 /* EIOSFileDownloaderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769D3E9D67A36F06CF0EFB4 /* EIOSFileDownloaderState.swift */; }; - E769D339FCECEE79EEDD55C5 /* IOSListenerForFileUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769D8D446B42BE572E246D4 /* IOSListenerForFileUploader.swift */; }; + E769D339FCECEE79EEDD55C5 /* McuMgrExceptionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769D8D446B42BE572E246D4 /* McuMgrExceptionHelpers.swift */; }; E769D36EAF2DD40E0A892DB2 /* DummyPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769DC33E51C5D88437572EB /* DummyPlaceholder.swift */; }; E769D37F2EF53CC9C5858BED /* IOSListenerForFirmwareInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769D13250C8CDA8CA70A8B7 /* IOSListenerForFirmwareInstaller.swift */; }; E769D3F56B12BF1FA2695F1E /* IOSListenerForFileDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769D19F5EC3404138BFDFF8 /* IOSListenerForFileDownloader.swift */; }; @@ -35,8 +38,11 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + B04DC3642CD3C7810048B553 /* EIOSDeviceResetInitializationVerdict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIOSDeviceResetInitializationVerdict.swift; sourceTree = ""; }; + B04DC3662CD3E1A20048B553 /* EIOSFirmwareErasureInitializationVerdict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EIOSFirmwareErasureInitializationVerdict.swift; sourceTree = ""; }; B0C562262936567900B070BA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; B0C562282936568800B070BA /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk/System/Library/Frameworks/CoreBluetooth.framework; sourceTree = DEVELOPER_DIR; }; + B0FF8AE82CCFF8E1004B39DE /* IOSListenerForFileUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSListenerForFileUploader.swift; sourceTree = ""; }; E769D13250C8CDA8CA70A8B7 /* IOSListenerForFirmwareInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IOSListenerForFirmwareInstaller.swift; sourceTree = ""; }; E769D19F5EC3404138BFDFF8 /* IOSListenerForFileDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IOSListenerForFileDownloader.swift; sourceTree = ""; }; E769D1DEDAD5F89C361DF28C /* EIOSFirmwareInstallationVerdict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIOSFirmwareInstallationVerdict.swift; sourceTree = ""; }; @@ -47,7 +53,7 @@ E769D5CB3083656E75BFBC1A /* IOSFirmwareEraser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IOSFirmwareEraser.swift; sourceTree = ""; }; E769D797570DEED2DFC21B70 /* EIOSFileUploadingInitializationVerdict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIOSFileUploadingInitializationVerdict.swift; sourceTree = ""; }; E769D8C352CB06D01D5C1EC7 /* EIOSFileDownloadingInitializationVerdict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIOSFileDownloadingInitializationVerdict.swift; sourceTree = ""; }; - E769D8D446B42BE572E246D4 /* IOSListenerForFileUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IOSListenerForFileUploader.swift; sourceTree = ""; }; + E769D8D446B42BE572E246D4 /* McuMgrExceptionHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = McuMgrExceptionHelpers.swift; sourceTree = ""; }; E769D93618819B3E676FE0EA /* IOSListenerForDeviceResetter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IOSListenerForDeviceResetter.swift; sourceTree = ""; }; E769D97697BB244952CDDF0E /* McuMgrBindingsiOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = McuMgrBindingsiOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E769D9B93897FF4C9ABBAF94 /* IOSFileDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IOSFileDownloader.swift; sourceTree = ""; }; @@ -98,6 +104,8 @@ E769DC768A6D6F0D1CAC3C06 /* McuMgrBindingsiOS.h */, E769D3E2D33EE281518619A7 /* IOSFirmwareInstaller.swift */, E769D1DEDAD5F89C361DF28C /* EIOSFirmwareInstallationVerdict.swift */, + B04DC3642CD3C7810048B553 /* EIOSDeviceResetInitializationVerdict.swift */, + B04DC3662CD3E1A20048B553 /* EIOSFirmwareErasureInitializationVerdict.swift */, E769DC8A37490B096716C6B8 /* EIOSFirmwareInstallationMode.swift */, E769DCEDC8442691AF65E8F8 /* EIOSFirmwareInstallationState.swift */, E769DB7090742CC7D6AFD01D /* InvalidFirmwareInstallationModeError.swift */, @@ -113,12 +121,13 @@ E769D9D0270D6C4E5DF8E7CE /* EIOSFileUploaderState.swift */, E769D417A16B8DCE9D59165A /* IOSFileUploader.swift */, E769D797570DEED2DFC21B70 /* EIOSFileUploadingInitializationVerdict.swift */, - E769D8D446B42BE572E246D4 /* IOSListenerForFileUploader.swift */, + E769D8D446B42BE572E246D4 /* McuMgrExceptionHelpers.swift */, E769D9B93897FF4C9ABBAF94 /* IOSFileDownloader.swift */, E769D3E9D67A36F06CF0EFB4 /* EIOSFileDownloaderState.swift */, E769D19F5EC3404138BFDFF8 /* IOSListenerForFileDownloader.swift */, E769D8C352CB06D01D5C1EC7 /* EIOSFileDownloadingInitializationVerdict.swift */, E769DD92BBBF9A69EAF334F4 /* EIOSFirmwareInstallationFatalError.swift */, + B0FF8AE82CCFF8E1004B39DE /* IOSListenerForFileUploader.swift */, ); path = McuMgrBindingsiOS; sourceTree = ""; @@ -234,12 +243,15 @@ E769D37F2EF53CC9C5858BED /* IOSListenerForFirmwareInstaller.swift in Sources */, E769D36EAF2DD40E0A892DB2 /* DummyPlaceholder.swift in Sources */, E769D8E49B6F52BD0065A849 /* EIOSFileUploaderState.swift in Sources */, + B04DC3652CD3C7810048B553 /* EIOSDeviceResetInitializationVerdict.swift in Sources */, E769DA011D5897CB39E02602 /* IOSFileUploader.swift in Sources */, E769DEB3EFF58D41D88197C4 /* EIOSFileUploadingInitializationVerdict.swift in Sources */, - E769D339FCECEE79EEDD55C5 /* IOSListenerForFileUploader.swift in Sources */, + E769D339FCECEE79EEDD55C5 /* McuMgrExceptionHelpers.swift in Sources */, E769D7F106E9BC190CDFFD19 /* IOSFileDownloader.swift in Sources */, + B0FF8AE92CCFF8E1004B39DE /* IOSListenerForFileUploader.swift in Sources */, E769D33171D78172F096C8A4 /* EIOSFileDownloaderState.swift in Sources */, E769D3F56B12BF1FA2695F1E /* IOSListenerForFileDownloader.swift in Sources */, + B04DC3672CD3E1A20048B553 /* EIOSFirmwareErasureInitializationVerdict.swift in Sources */, E769D54A54C788CCE8488B9B /* EIOSFileDownloadingInitializationVerdict.swift in Sources */, E769D5AE4CFECFEA0B509626 /* EIOSFirmwareInstallationFatalError.swift in Sources */, ); diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSDeviceResetInitializationVerdict.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSDeviceResetInitializationVerdict.swift new file mode 100644 index 00000000..41f50caa --- /dev/null +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSDeviceResetInitializationVerdict.swift @@ -0,0 +1,6 @@ +@objc +public enum EIOSDeviceResetInitializationVerdict: Int { + case success = 0 + case failedErrorUponCommencing = 1 + case failedOtherResetAlreadyInProgress = 2 +} diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileDownloadingInitializationVerdict.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileDownloadingInitializationVerdict.swift index 422a03d4..1f2a2013 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileDownloadingInitializationVerdict.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileDownloadingInitializationVerdict.swift @@ -2,5 +2,6 @@ public enum EIOSFileDownloadingInitializationVerdict: Int { case success = 0 case failedInvalidSettings = 1 - case failedDownloadAlreadyInProgress = 2 + case failedErrorUponCommencing = 2 + case failedDownloadAlreadyInProgress = 3 } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileUploadingInitializationVerdict.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileUploadingInitializationVerdict.swift index c3046b4f..28eab1a5 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileUploadingInitializationVerdict.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFileUploadingInitializationVerdict.swift @@ -1,7 +1,8 @@ @objc public enum EIOSFileUploadingInitializationVerdict: Int { case success = 0 - case failedInvalidSettings = 1 - case failedInvalidData = 2 - case failedOtherUploadAlreadyInProgress = 3 + case failedInvalidData = 1 + case failedInvalidSettings = 2 + case failedErrorUponCommencing = 3 + case failedOtherUploadAlreadyInProgress = 4 } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFirmwareErasureInitializationVerdict.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFirmwareErasureInitializationVerdict.swift new file mode 100644 index 00000000..d88d44c8 --- /dev/null +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFirmwareErasureInitializationVerdict.swift @@ -0,0 +1,6 @@ +@objc +public enum EIOSFirmwareErasureInitializationVerdict: Int { + case success = 0 + case failedErrorUponCommencing = 1 + case failedOtherErasureAlreadyInProgress = 2 +} diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFirmwareInstallationFatalError.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFirmwareInstallationFatalError.swift index b84c3cc4..ef49787d 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFirmwareInstallationFatalError.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/EIOSFirmwareInstallationFatalError.swift @@ -7,4 +7,5 @@ public enum EIOSFirmwareInstallerFatalErrorType : Int //this must mirror the enu case deploymentFailed = 3 case firmwareImageSwapTimeout = 4 case firmwareUploadingErroredOut = 5 + case failedInstallationAlreadyInProgress = 6 } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSDeviceResetter.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSDeviceResetter.swift index 853fce64..6a1d4b3a 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSDeviceResetter.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSDeviceResetter.swift @@ -17,31 +17,53 @@ public class IOSDeviceResetter: NSObject { } @objc - public func beginReset() { - setState(EIOSDeviceResetterState.resetting) + public func beginReset(_ keepThisDummyParameter: Bool = false) -> EIOSDeviceResetInitializationVerdict { + if (!isCold()) { //keep first + onError("[IOSDR.BR.000] Another erasure operation is already in progress") - _manager = DefaultManager(transport: _transporter) - _manager.logDelegate = self + return .failedOtherResetAlreadyInProgress + } - _manager.reset { - response, error in + do { + setState(.idle) + _manager = DefaultManager(transport: _transporter) + _manager.logDelegate = self - if (error != nil) { - self.fatalErrorOccurredAdvertisement("Reset failed: '\(error?.localizedDescription ?? "")'") + setState(.resetting) + _manager.reset { + response, error in - self.setState(EIOSDeviceResetterState.failed) - return - } + if (error != nil) { + self.onError("[IOSDR.BR.010] Reset failed: '\(error?.localizedDescription ?? "")'", error) + return + } - if (response?.getError() != nil) { // check for an error return code - self.fatalErrorOccurredAdvertisement("Reset failed: '\(response?.getError()?.errorDescription ?? "N/A")'") + if (response?.getError() != nil) { // check for an error return code + self.onError("[IOSDR.BR.020] Reset failed: '\(response?.getError()?.errorDescription ?? "")'", response?.getError()) + return + } - self.setState(EIOSDeviceResetterState.failed) - return + self.setState(.complete) } + } catch let ex { + onError("[IOSDR.BR.030] Failed to launch the installation process: '\(ex.localizedDescription)", ex) - self.setState(EIOSDeviceResetterState.complete) + return .failedErrorUponCommencing } + + return .success + } + + private func isCold() -> Bool { + return _currentState == .none + || _currentState == .failed + || _currentState == .complete + } + + private func onError(_ errorMessage: String, _ error: Error? = nil) { + setState(.failed) + + fatalErrorOccurredAdvertisement(errorMessage, McuMgrExceptionHelpers.deduceGlobalErrorCodeFromException(error)) } @objc @@ -76,10 +98,10 @@ public class IOSDeviceResetter: NSObject { } //@objc dont - private func fatalErrorOccurredAdvertisement(_ errorMessage: String) { + private func fatalErrorOccurredAdvertisement(_ errorMessage: String, _ globalErrorCode: Int) { _lastFatalErrorMessage = errorMessage - _listener.fatalErrorOccurredAdvertisement(errorMessage) + _listener.fatalErrorOccurredAdvertisement(errorMessage, globalErrorCode) } //@objc dont diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileDownloader.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileDownloader.swift index 49555c18..76715b72 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileDownloader.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileDownloader.swift @@ -6,73 +6,97 @@ public class IOSFileDownloader: NSObject { private var _listener: IOSListenerForFileDownloader! private var _transporter: McuMgrBleTransport! - private var _currentState: EIOSFileDownloaderState + private var _cbPeripheral: CBPeripheral! private var _fileSystemManager: FileSystemManager! - private var _lastFatalErrorMessage: String + private var _currentState: EIOSFileDownloaderState = .none private var _lastBytesSend: Int = -1 + private var _lastFatalErrorMessage: String = "" private var _lastBytesSendTimestamp: Date? = nil - private var _remoteFilePathSanitized: String + private var _remoteFilePathSanitized: String = "" + + @objc + public init(_ listener: IOSListenerForFileDownloader!) { + _listener = listener + } @objc public init(_ cbPeripheral: CBPeripheral!, _ listener: IOSListenerForFileDownloader!) { _listener = listener - _transporter = McuMgrBleTransport(cbPeripheral) - _currentState = .none - _lastFatalErrorMessage = "" - _remoteFilePathSanitized = "" + _cbPeripheral = cbPeripheral } @objc - public func beginDownload(_ remoteFilePath: String) -> EIOSFileDownloadingInitializationVerdict { - if _currentState != .none - && _currentState != .error - && _currentState != .complete - && _currentState != .cancelled { //if another download is already in progress we bail out - return EIOSFileDownloadingInitializationVerdict.failedDownloadAlreadyInProgress + public func trySetBluetoothDevice(_ cbPeripheral: CBPeripheral!) -> Bool { + if !isIdleOrCold() { + return false } - _lastBytesSend = -1 - _lastBytesSendTimestamp = nil + if !tryInvalidateCachedTransport() { //order + return false + } - if remoteFilePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - setState(EIOSFileDownloaderState.error) - fatalErrorOccurredAdvertisement("", "Target-file provided is dud!") + _cbPeripheral = cbPeripheral //order + return true + } + + @objc + public func tryInvalidateCachedTransport() -> Bool { + if _transporter == nil { //already scrapped + return true + } - return EIOSFileDownloadingInitializationVerdict.failedInvalidSettings + if !isIdleOrCold() { //if the upload is already in progress we bail out + return false } - if remoteFilePath.hasSuffix("/") { - setState(EIOSFileDownloaderState.error) - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, "Target-file points to a directory instead of a file") + disposeFilesystemManager() // order + disposeTransport() // order - return EIOSFileDownloadingInitializationVerdict.failedInvalidSettings + return true; + } + + @objc + public func beginDownload(_ remoteFilePath: String) -> EIOSFileDownloadingInitializationVerdict { + + if !isCold() { //keep first if another download is already in progress we bail out + onError("[IOSFD.BD.010] Another download is already in progress") + + return .failedDownloadAlreadyInProgress } - if !remoteFilePath.hasPrefix("/") { - setState(EIOSFileDownloaderState.error) - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, "Target-path is not absolute!") + _remoteFilePathSanitized = remoteFilePath.trimmingCharacters(in: .whitespacesAndNewlines) + if _remoteFilePathSanitized.isEmpty { + onError("[IOSFD.BD.020] Target-file provided is dud!") - return EIOSFileDownloadingInitializationVerdict.failedInvalidSettings + return .failedInvalidSettings } - _fileSystemManager = FileSystemManager(transport: _transporter) // the delegate aspect is implemented in the extension below - _fileSystemManager.logDelegate = self + if _remoteFilePathSanitized.hasSuffix("/") { + onError("[IOSFD.BD.030] Target-file points to a directory instead of a file") - setState(EIOSFileDownloaderState.idle) - busyStateChangedAdvertisement(true) - fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0) + return .failedInvalidSettings + } + + if !_remoteFilePathSanitized.hasPrefix("/") { + onError("[IOSFD.BD.040] Target-path is not absolute!") - _remoteFilePathSanitized = remoteFilePath - let success = _fileSystemManager.download(name: remoteFilePath, delegate: self) + return .failedInvalidSettings + } + + resetUploadState() //order + disposeFilesystemManager() //00 vital hack + ensureTransportIsInitializedExactlyOnce() //order + ensureFilesystemManagerIsInitializedExactlyOnce() //order + + let success = _fileSystemManager.download(name: _remoteFilePathSanitized, delegate: self) if !success { - setState(EIOSFileDownloaderState.error) - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, "Failed to commence file-Downloading (check logs for details)") + onError("[IOSFD.BD.050] Failed to commence file-Downloading (check logs for details)") - return EIOSFileDownloadingInitializationVerdict.failedInvalidSettings + return .failedErrorUponCommencing } - return EIOSFileDownloadingInitializationVerdict.success + return .success } @objc @@ -106,11 +130,64 @@ public class IOSFileDownloader: NSObject { _transporter?.close() } - //@objc dont - private func fatalErrorOccurredAdvertisement(_ resource: String, _ errorMessage: String) { - _lastFatalErrorMessage = errorMessage + private func isIdleOrCold() -> Bool { + return _currentState == .idle || isCold(); + } - _listener.fatalErrorOccurredAdvertisement(resource, errorMessage) + private func isCold() -> Bool { + return _currentState == .none + || _currentState == .error + || _currentState == .complete + || _currentState == .cancelled + } + + private func resetUploadState() { + _lastBytesSend = -1 + _lastBytesSendTimestamp = nil + + setState(.idle) + busyStateChangedAdvertisement(true) + fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0) + } + + private func ensureFilesystemManagerIsInitializedExactlyOnce() { + if _fileSystemManager != nil { //already initialized + return + } + + _fileSystemManager = FileSystemManager(transport: _transporter) //00 + _fileSystemManager.logDelegate = self //00 + + //00 this doesnt throw an error the log-delegate aspect is implemented in the extension below via IOSFileDownloader: McuMgrLogDelegate + } + + private func ensureTransportIsInitializedExactlyOnce() { + if _transporter != nil { + return + } + + _transporter = McuMgrBleTransport(_cbPeripheral) + } + + private func disposeTransport() { + _transporter?.close() + _transporter = nil + } + + private func disposeFilesystemManager() { + //_fileSystemManager?.cancelTransfer() dont + _fileSystemManager = nil + } + + //@objc dont + private func onError(_ errorMessage: String, _ error: Error? = nil) { + _lastFatalErrorMessage = errorMessage // order + setState(.error) // order + _listener.fatalErrorOccurredAdvertisement( // order + _remoteFilePathSanitized, + errorMessage, + McuMgrExceptionHelpers.deduceGlobalErrorCodeFromException(error) + ) } //@objc dont @@ -143,7 +220,7 @@ public class IOSFileDownloader: NSObject { ) { _listener.fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage, averageThroughput) } - + //@objc dont private func downloadCompletedAdvertisement(_ resource: String, _ data: [UInt8]) { _listener.downloadCompletedAdvertisement(resource, data) @@ -160,7 +237,7 @@ public class IOSFileDownloader: NSObject { stateChangedAdvertisement(oldState, newState) //order - if (oldState == EIOSFileDownloaderState.downloading && newState == EIOSFileDownloaderState.complete) //00 + if (oldState == .downloading && newState == .complete) //00 { fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(100, 0) } @@ -171,27 +248,27 @@ public class IOSFileDownloader: NSObject { extension IOSFileDownloader: FileDownloadDelegate { public func downloadProgressDidChange(bytesDownloaded bytesSent: Int, fileSize: Int, timestamp: Date) { - setState(EIOSFileDownloaderState.downloading) + setState(.downloading) let throughputKilobytesPerSecond = calculateThroughput(bytesSent: bytesSent, timestamp: timestamp) let DownloadProgressPercentage = (bytesSent * 100) / fileSize fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(DownloadProgressPercentage, throughputKilobytesPerSecond) } public func downloadDidFail(with error: Error) { - setState(EIOSFileDownloaderState.error) - fatalErrorOccurredAdvertisement(_remoteFilePathSanitized, error.localizedDescription) + onError(error.localizedDescription, error) + busyStateChangedAdvertisement(false) } public func downloadDidCancel() { - setState(EIOSFileDownloaderState.cancelled) + setState(.cancelled) busyStateChangedAdvertisement(false) fileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0) cancelledAdvertisement() } public func download(of name: String, didFinish data: Data) { - setState(EIOSFileDownloaderState.complete) + setState(.complete) downloadCompletedAdvertisement(_remoteFilePathSanitized, [UInt8](data)) busyStateChangedAdvertisement(false) } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileUploader.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileUploader.swift index 62aaa6c4..3b18cbc5 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileUploader.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFileUploader.swift @@ -7,13 +7,14 @@ public class IOSFileUploader: NSObject { private let _listener: IOSListenerForFileUploader! private var _transporter: McuMgrBleTransport! private var _cbPeripheral: CBPeripheral! - private var _currentState: EIOSFileUploaderState = .none private var _fileSystemManager: FileSystemManager! - private var _lastFatalErrorMessage: String = "" - private var _remoteFilePathSanitized: String! + private var _currentState: EIOSFileUploaderState = .none private var _lastBytesSend: Int = 0 + private var _cancellationReason: String = "" + private var _lastFatalErrorMessage: String = "" private var _lastBytesSendTimestamp: Date? = nil + private var _remoteFilePathSanitized: String! @objc public init(_ listener: IOSListenerForFileUploader!) { @@ -28,7 +29,7 @@ public class IOSFileUploader: NSObject { @objc public func trySetBluetoothDevice(_ cbPeripheral: CBPeripheral!) -> Bool { - if !IsCold() { + if !isIdleOrCold() { return false } @@ -46,7 +47,7 @@ public class IOSFileUploader: NSObject { return true } - if !IsCold() { //if the upload is already in progress we bail out + if !isIdleOrCold() { //if the upload is already in progress we bail out return false } @@ -57,67 +58,107 @@ public class IOSFileUploader: NSObject { } @objc - public func beginUpload(_ remoteFilePath: String, _ data: Data) -> EIOSFileUploadingInitializationVerdict { - if !IsCold() { //if another upload is already in progress we bail out - setState(EIOSFileUploaderState.error) - onError("Another upload is already in progress") + public func beginUpload( + _ remoteFilePath: String, + _ data: Data?, + _ pipelineDepth: Int, + _ byteAlignment: Int + ) -> EIOSFileUploadingInitializationVerdict { - return EIOSFileUploadingInitializationVerdict.failedOtherUploadAlreadyInProgress - } - - if _cbPeripheral == nil { - setState(EIOSFileUploaderState.error); - onError("No bluetooth-device specified - call trySetBluetoothDevice() first"); + if !isCold() { //keep first if another upload is already in progress we bail out + onError("[IOSFU.BU.010] Another upload is already in progress") - return EIOSFileUploadingInitializationVerdict.failedInvalidSettings; + return .failedOtherUploadAlreadyInProgress } _remoteFilePathSanitized = remoteFilePath.trimmingCharacters(in: .whitespacesAndNewlines) if _remoteFilePathSanitized.isEmpty { - setState(EIOSFileUploaderState.error) - onError("Target-file provided is dud") + onError("[IOSFU.BU.020] Target-file provided is dud") - return EIOSFileUploadingInitializationVerdict.failedInvalidSettings + return .failedInvalidSettings } if _remoteFilePathSanitized.hasSuffix("/") { - setState(EIOSFileUploaderState.error) - onError("Target-file points to a directory instead of a file") + onError("[IOSFU.BU.030] Target-file points to a directory instead of a file") - return EIOSFileUploadingInitializationVerdict.failedInvalidSettings + return .failedInvalidSettings } if !_remoteFilePathSanitized.hasPrefix("/") { - setState(EIOSFileUploaderState.error) - onError("Target-path is not absolute!") + onError("[IOSFU.BU.040] Target-path is not absolute") - return EIOSFileUploadingInitializationVerdict.failedInvalidSettings + return .failedInvalidSettings } - // if data == nil { // data being nil is not ok but in swift Data can never be nil anyway btw data.length==0 is perfectly ok because we might want to create empty files - // return EIOSFileUploaderVerdict.FAILED__INVALID_DATA - // } + if data == nil { // data being nil is not ok btw data.length==0 is perfectly ok because we might want to create empty files + onError("[IOSFU.BU.050] The data provided are nil") - disposeFilesystemManager() //vital hack normally we shouldnt need this but there seems to be a bug in the lib https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-Manager/issues/209 + return .failedInvalidData + } - ensureTransportIsInitializedExactlyOnce() //order - ensureFilesystemManagerIsInitializedExactlyOnce() //order + if _cbPeripheral == nil { + onError("[IOSFU.BU.060] No bluetooth-device specified - call trySetBluetoothDevice() first"); + + return .failedInvalidSettings; + } + + if (pipelineDepth >= 2 && byteAlignment <= 1) { + onError("[IOSFU.BU.070] When pipeline-depth is set to 2 or above you must specify a byte-alignment >=2 (given byte-alignment is '\(byteAlignment)')") + + return .failedInvalidSettings + } + + let byteAlignmentEnum = translateByteAlignmentMode(byteAlignment); + if (byteAlignmentEnum == nil) { + onError("[IOSFU.BU.080] Invalid byte-alignment value '\(byteAlignment)': It must be a power of 2 up to 16") + + return .failedInvalidSettings + } resetUploadState() //order + disposeFilesystemManager() //00 vital hack + ensureTransportIsInitializedExactlyOnce() //order + ensureFilesystemManagerIsInitializedExactlyOnce() //order + var configuration = FirmwareUpgradeConfiguration(byteAlignment: byteAlignmentEnum!) + if (pipelineDepth >= 0) { + configuration.pipelineDepth = pipelineDepth + } + let success = _fileSystemManager.upload( //order - name: _remoteFilePathSanitized, - data: data, - delegate: self + name: _remoteFilePathSanitized, + data: data!, + using: configuration, + delegate: self ) if !success { - setState(EIOSFileUploaderState.error) - onError("Failed to commence file-uploading (check logs for details)") + onError("[IOSFU.BU.090] Failed to commence file-uploading (check logs for details)") - return EIOSFileUploadingInitializationVerdict.failedInvalidSettings + return .failedErrorUponCommencing } - return EIOSFileUploadingInitializationVerdict.success + return .success + + //00 normally we shouldnt need this but there seems to be a bug in the lib https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-Manager/issues/209 + } + + private func translateByteAlignmentMode(_ alignment: Int) -> ImageUploadAlignment? { + if (alignment <= 0) { + return .disabled; + } + + switch alignment { + case 2: + return .twoByte + case 4: + return .fourByte + case 8: + return .eightByte + case 16: + return .sixteenByte + default: + return nil + } } @objc @@ -140,7 +181,10 @@ public class IOSFileUploader: NSObject { } @objc - public func cancel() { + public func cancel(_ reason: String = "") { + _cancellationReason = reason + + cancellingAdvertisement(reason) setState(.cancelling) //order _fileSystemManager?.cancelTransfer() //order @@ -155,7 +199,7 @@ public class IOSFileUploader: NSObject { _lastBytesSend = 0 _lastBytesSendTimestamp = nil - setState(EIOSFileUploaderState.idle) + setState(.idle) busyStateChangedAdvertisement(true) fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0) } @@ -189,66 +233,42 @@ public class IOSFileUploader: NSObject { _fileSystemManager = nil } - private func IsCold() -> Bool { - return _currentState == EIOSFileUploaderState.none - || _currentState == EIOSFileUploaderState.error - || _currentState == EIOSFileUploaderState.complete - || _currentState == EIOSFileUploaderState.cancelled + private func isIdleOrCold() -> Bool { + return _currentState == .idle || isCold(); + } + + private func isCold() -> Bool { + return _currentState == .none + || _currentState == .error + || _currentState == .complete + || _currentState == .cancelled } //@objc dont private func onError(_ errorMessage: String, _ error: Error? = nil) { _lastFatalErrorMessage = errorMessage - let (errorCode, _) = deduceErrorCode(errorMessage) - - _listener.fatalErrorOccurredAdvertisement( + setState(.error) // order + _listener.fatalErrorOccurredAdvertisement( // order _remoteFilePathSanitized, errorMessage, - errorCode + McuMgrExceptionHelpers.deduceGlobalErrorCodeFromException(error) ) } - // unfortunately I couldnt figure out a way to deduce the error code from the error itself so I had to resort to string sniffing ugly but it works - private func deduceErrorCode(_ errorMessage: String) -> (Int, String?) { - let (matchesArray, possibleError) = matches(for: " [(]\\d+[)][.]?$", in: errorMessage) // "UNKNOWN (1)." - if possibleError != nil { - return (-99, possibleError) - } - - let errorCode = matchesArray.isEmpty - ? -99 - : (Int(matchesArray[0].trimmingCharacters(in: .whitespaces).trimmingCharacters(in: [ "(", ")", "." ]).trimmingCharacters(in: .whitespaces)) ?? 0) - - return (errorCode, possibleError) - } - - private func matches(for regex: String, in text: String) -> ([String], String?) { //00 - do { - let regex = try NSRegularExpression(pattern: regex) - let results = regex.matches(in: text, range: NSRange(text.startIndex..., in: text)) - - return ( - results.map { String(text[Range($0.range, in: text)!]) }, - nil - ) - } catch let error { - print("invalid regex: \(error.localizedDescription)") - - return ([], error.localizedDescription) - } - - //00 https://stackoverflow.com/a/27880748/863651 - } - //@objc dont private func logMessageAdvertisement(_ message: String, _ category: String, _ level: String) { _listener.logMessageAdvertisement(message, category, level, _remoteFilePathSanitized) } //@objc dont - private func cancelledAdvertisement() { - _listener.cancelledAdvertisement() + private func cancellingAdvertisement(_ reason: String) { + _listener.cancellingAdvertisement(reason) + } + + //@objc dont + private func cancelledAdvertisement(_ reason: String) { + _listener.cancelledAdvertisement(reason) } //@objc dont @@ -291,7 +311,7 @@ public class IOSFileUploader: NSObject { stateChangedAdvertisement(oldState, newState) //order - if (oldState == EIOSFileUploaderState.uploading && newState == EIOSFileUploaderState.complete) //00 + if (oldState == .uploading && newState == .complete) //00 { fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(100, 0) } @@ -303,27 +323,26 @@ public class IOSFileUploader: NSObject { extension IOSFileUploader: FileUploadDelegate { public func uploadProgressDidChange(bytesSent: Int, fileSize: Int, timestamp: Date) { - setState(EIOSFileUploaderState.uploading) + setState(.uploading) let throughputKilobytesPerSecond = calculateThroughput(bytesSent: bytesSent, timestamp: timestamp) let uploadProgressPercentage = (bytesSent * 100) / fileSize fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(uploadProgressPercentage, throughputKilobytesPerSecond) } public func uploadDidFail(with error: Error) { - setState(EIOSFileUploaderState.error) onError(error.localizedDescription, error) busyStateChangedAdvertisement(false) } public func uploadDidCancel() { - setState(EIOSFileUploaderState.cancelled) + setState(.cancelled) busyStateChangedAdvertisement(false) fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(0, 0) - cancelledAdvertisement() + cancelledAdvertisement(_cancellationReason) } public func uploadDidFinish() { - setState(EIOSFileUploaderState.complete) + setState(.complete) fileUploadedAdvertisement() busyStateChangedAdvertisement(false) } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareEraser.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareEraser.swift index 164645d5..dd2a4ed8 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareEraser.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareEraser.swift @@ -17,26 +17,39 @@ public class IOSFirmwareEraser: NSObject { } @objc - public func beginErasure(_ imageIndex: Int) { - busyStateChangedAdvertisement(true) + public func beginErasure(_ imageIndex: Int) -> EIOSFirmwareErasureInitializationVerdict { + if (!isCold()) { //keep first + onError("[IOSFE.BE.000] Another erasure operation is already in progress"); - setState(EIOSFirmwareEraserState.erasing) + return .failedOtherErasureAlreadyInProgress; + } - _manager = ImageManager(transport: _transporter) - _manager.logDelegate = self + do + { + setState(.idle) + _manager = ImageManager(transport: _transporter) + _manager.logDelegate = self - _manager.erase { - response, error in - if (error != nil) { - self.fatalErrorOccurredAdvertisement(error?.localizedDescription ?? "An unspecified error occurred") - self.busyStateChangedAdvertisement(false) - self.setState(EIOSFirmwareEraserState.failed) - return + setState(.erasing) + busyStateChangedAdvertisement(true) + _manager.erase { + response, error in + + if (error != nil) { + self.onError("[IOSFE.BE.010] Failed to start firmware erasure: \(error?.localizedDescription ?? "An unspecified error occurred")", error) + return + } + + self.readImageErasure() + self.setState(.complete) } + } catch let ex { + onError("[IOSFE.BE.020] Failed to launch the installation process: '\(ex.localizedDescription)", ex) - self.readImageErasure() - self.setState(EIOSFirmwareEraserState.complete) + return .failedErrorUponCommencing } + + return .success } @objc @@ -44,6 +57,18 @@ public class IOSFirmwareEraser: NSObject { _transporter?.close() } + private func isCold() -> Bool { + return _currentState == .none + || _currentState == .failed + || _currentState == .complete + } + + private func onError(_ errorMessage: String, _ error: Error? = nil) { + setState(.failed) + busyStateChangedAdvertisement(false) + fatalErrorOccurredAdvertisement(errorMessage, McuMgrExceptionHelpers.deduceGlobalErrorCodeFromException(error)) + } + private var _lastFatalErrorMessage: String @objc @@ -60,10 +85,9 @@ public class IOSFirmwareEraser: NSObject { } //@objc dont - private func fatalErrorOccurredAdvertisement(_ errorMessage: String) { + private func fatalErrorOccurredAdvertisement(_ errorMessage: String, _ globalErrorCode: Int) { _lastFatalErrorMessage = errorMessage //this method is meant to be overridden by csharp binding libraries to intercept updates - - _listener.fatalErrorOccurredAdvertisement(errorMessage) + _listener.fatalErrorOccurredAdvertisement(errorMessage, globalErrorCode) } //@objc dont @@ -90,9 +114,9 @@ public class IOSFirmwareEraser: NSObject { _manager.list { response, error in + if (error != nil) { - self.fatalErrorOccurredAdvertisement(error?.localizedDescription ?? "An unspecified error occurred") - self.busyStateChangedAdvertisement(false) + self.onError("[IOSFE.RIE.010] Failed to read firmware images after firmware-erasure completed: \(error?.localizedDescription ?? "An unspecified error occurred")", error) return } @@ -113,4 +137,4 @@ extension IOSFirmwareEraser: McuMgrLogDelegate { level.name ) } -} \ No newline at end of file +} diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareInstaller.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareInstaller.swift index 12cfd21e..079f62e8 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareInstaller.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSFirmwareInstaller.swift @@ -30,7 +30,9 @@ public class IOSFirmwareInstaller: NSObject { _ pipelineDepth: Int, _ byteAlignment: Int ) -> EIOSFirmwareInstallationVerdict { - if _currentState != .none && _currentState != .cancelled && _currentState != .complete && _currentState != .error { //if another installation is already in progress we bail out + if !isCold() { //if another installation is already in progress we bail out + onError(.failedInstallationAlreadyInProgress, "[IOSFI.BI.000] Another firmware installation is already in progress") + return .failedInstallationAlreadyInProgress } @@ -38,27 +40,27 @@ public class IOSFirmwareInstaller: NSObject { _lastBytesSendTimestamp = nil if (imageData.isEmpty) { - emitFatalError(.invalidFirmware, "The firmware data-bytes given are dud!") + onError(.invalidFirmware, "[IOSFI.BI.010] The firmware data-bytes given are dud") return .failedInvalidFirmware } if (pipelineDepth >= 2 && byteAlignment <= 1) { - emitFatalError(.invalidSettings, "When pipeline-depth is set to 2 or above you must specify a byte-alignment >=2 (given byte-alignment is '\(byteAlignment)')") + onError(.invalidSettings, "[IOSFI.BI.020] When pipeline-depth is set to 2 or above you must specify a byte-alignment >=2 (given byte-alignment is '\(byteAlignment)')") return .failedInvalidSettings } let byteAlignmentEnum = translateByteAlignmentMode(byteAlignment); if (byteAlignmentEnum == nil) { - emitFatalError(.invalidSettings, "Invalid byte-alignment value '\(byteAlignment)': It must be a power of 2 up to 16") + onError(.invalidSettings, "[IOSFI.BI.030] Invalid byte-alignment value '\(byteAlignment)': It must be a power of 2 up to 16") return .failedInvalidSettings } if (estimatedSwapTimeInMilliseconds >= 0 && estimatedSwapTimeInMilliseconds <= 1000) { //its better to just warn the calling environment instead of erroring out logMessageAdvertisement( - "Estimated swap-time of '\(estimatedSwapTimeInMilliseconds)' milliseconds seems suspiciously low - did you mean to say '\(estimatedSwapTimeInMilliseconds * 1000)' milliseconds?", + "[IOSFI.BI.040] Estimated swap-time of '\(estimatedSwapTimeInMilliseconds)' milliseconds seems suspiciously low - did you mean to say '\(estimatedSwapTimeInMilliseconds * 1000)' milliseconds?", "firmware-installer", iOSMcuManagerLibrary.McuMgrLogLevel.warning.name ) @@ -84,7 +86,7 @@ public class IOSFirmwareInstaller: NSObject { } } catch let ex { - emitFatalError(.invalidSettings, ex.localizedDescription) + onError(.invalidSettings, "[IOSFI.BI.050] Failed to configure the firmware-installer: '\(ex.localizedDescription)") return .failedInvalidSettings } @@ -93,17 +95,19 @@ public class IOSFirmwareInstaller: NSObject { setState(.idle) try _manager.start( - images: [ImageManager.Image( //2 - image: 0, - slot: 1, - hash: try McuMgrImage(data: imageData).hash, - data: imageData - )], - using: firmwareUpgradeConfiguration + images: [ + ImageManager.Image( //2 + image: 0, + slot: 1, + hash: try McuMgrImage(data: imageData).hash, + data: imageData + ) + ], + using: firmwareUpgradeConfiguration ) } catch let ex { - emitFatalError(.deploymentFailed, ex.localizedDescription) + onError(.deploymentFailed, "[IOSFI.BI.060] Failed to launch the installation process: '\(ex.localizedDescription)") return .failedDeploymentError } @@ -116,6 +120,13 @@ public class IOSFirmwareInstaller: NSObject { // //2 the hashing algorithm is very specific to nordic there is no practical way to go about getting it other than using the McuMgrImage utility class } + + private func isCold() -> Bool { + return _currentState == .none + || _currentState == .error + || _currentState == .complete + || _currentState == .cancelled + } private func calculateHashBytesOfData(_ data: Data) -> Data { var hasher = Hasher() @@ -181,20 +192,24 @@ public class IOSFirmwareInstaller: NSObject { _transporter?.close() } - private func emitFatalError(_ fatalErrorType: EIOSFirmwareInstallerFatalErrorType, _ errorMessage: String) { - let currentStateSnapshot = _currentState //00 - - setState(.error) // order - fatalErrorOccurredAdvertisement(currentStateSnapshot, fatalErrorType, errorMessage) // order + private func onError(_ fatalErrorType: EIOSFirmwareInstallerFatalErrorType, _ errorMessage: String, _ error: Error? = nil) { + let currentStateSnapshot = _currentState //00 order + setState(.error) // order + fatalErrorOccurredAdvertisement( // order + currentStateSnapshot, + fatalErrorType, + errorMessage, + McuMgrExceptionHelpers.deduceGlobalErrorCodeFromException(error) + ) //00 we want to let the calling environment know in which exact state the fatal error happened in } //@objc dont - private func fatalErrorOccurredAdvertisement(_ currentState: EIOSFirmwareInstallationState, _ fatalErrorType: EIOSFirmwareInstallerFatalErrorType, _ errorMessage: String) { + private func fatalErrorOccurredAdvertisement(_ currentState: EIOSFirmwareInstallationState, _ fatalErrorType: EIOSFirmwareInstallerFatalErrorType, _ errorMessage: String, _ globalErrorCode: Int) { _lastFatalErrorMessage = errorMessage - _listener.fatalErrorOccurredAdvertisement(currentState, fatalErrorType, errorMessage) + _listener.fatalErrorOccurredAdvertisement(currentState, fatalErrorType, errorMessage, globalErrorCode) } //@objc dont @@ -301,12 +316,6 @@ extension IOSFirmwareInstaller: FirmwareUpgradeDelegate { //todo calculate thr } public func upgradeDidFail(inState state: FirmwareUpgradeState, with error: Error) { - logMessageAdvertisement( - "** upgradeDidFail: state=\(state), error-message=\(error.localizedDescription), error-type=\(type(of: error))", - "firmware-installer", - iOSMcuManagerLibrary.McuMgrLogLevel.debug.name - ) - var fatalErrorType = EIOSFirmwareInstallerFatalErrorType.generic if (state == .upload) { //todo improve this heuristic once we figure out the exact type of exception we get in case of an upload error fatalErrorType = .firmwareUploadingErroredOut @@ -315,7 +324,7 @@ extension IOSFirmwareInstaller: FirmwareUpgradeDelegate { //todo calculate thr fatalErrorType = .firmwareImageSwapTimeout } - emitFatalError(fatalErrorType, error.localizedDescription) + onError(fatalErrorType, error.localizedDescription, error) busyStateChangedAdvertisement(false) } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForDeviceResetter.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForDeviceResetter.swift index a9f2c8c1..efb6eb51 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForDeviceResetter.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForDeviceResetter.swift @@ -9,6 +9,6 @@ import Foundation @objc public protocol IOSListenerForDeviceResetter { func logMessageAdvertisement(_ message: String, _ category: String, _ level: String) - func fatalErrorOccurredAdvertisement(_ errorMessage: String) + func fatalErrorOccurredAdvertisement(_ errorMessage: String, _ globalErrorCode: Int) func stateChangedAdvertisement(_ oldState: EIOSDeviceResetterState, _ newState: EIOSDeviceResetterState) } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileDownloader.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileDownloader.swift index 62d9ea0c..c65efe14 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileDownloader.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileDownloader.swift @@ -3,9 +3,10 @@ import Foundation @objc public protocol IOSListenerForFileDownloader { func logMessageAdvertisement(_ message: String, _ category: String, _ level: String, _ resource: String) - func fatalErrorOccurredAdvertisement(_ resource: String, _ errorMessage: String) + func fatalErrorOccurredAdvertisement(_ resource: String, _ errorMessage: String, _ globalErrorCode: Int) func cancelledAdvertisement() + func stateChangedAdvertisement(_ resource: String, _ oldState: EIOSFileDownloaderState, _ newState: EIOSFileDownloaderState) func busyStateChangedAdvertisement(_ busyNotIdle: Bool) func downloadCompletedAdvertisement(_ resource: String, _ data: [UInt8]) diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileUploader.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileUploader.swift index cbda2471..698c60fe 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileUploader.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFileUploader.swift @@ -3,11 +3,14 @@ import Foundation @objc public protocol IOSListenerForFileUploader { func logMessageAdvertisement(_ message: String, _ category: String, _ level: String, _ resource: String) - func fatalErrorOccurredAdvertisement(_ resource: String, _ errorMessage: String, _ errorCode: Int) + func fatalErrorOccurredAdvertisement(_ resource: String, _ errorMessage: String, _ globalErrorCode: Int) + + func cancelledAdvertisement(_ reason: String) + func cancellingAdvertisement(_ reason: String) - func cancelledAdvertisement() func stateChangedAdvertisement(_ resource: String, _ oldState: EIOSFileUploaderState, _ newState: EIOSFileUploaderState) func fileUploadedAdvertisement(_ resource: String) func busyStateChangedAdvertisement(_ busyNotIdle: Bool) func fileUploadProgressPercentageAndDataThroughputChangedAdvertisement(_ progressPercentage: Int, _ averageThroughput: Float32) } + diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareEraser.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareEraser.swift index 49265884..cd6d37eb 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareEraser.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareEraser.swift @@ -9,7 +9,7 @@ import Foundation @objc public protocol IOSListenerForFirmwareEraser { func logMessageAdvertisement(_ message: String, _ category: String, _ level: String) - func fatalErrorOccurredAdvertisement(_ errorMessage: String) + func fatalErrorOccurredAdvertisement(_ errorMessage: String, _ globalErrorCode: Int) func stateChangedAdvertisement(_ oldState: EIOSFirmwareEraserState, _ newState: EIOSFirmwareEraserState) func busyStateChangedAdvertisement(_ busyNotIdle: Bool) } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareInstaller.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareInstaller.swift index 87626e5d..9ded75fe 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareInstaller.swift +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/IOSListenerForFirmwareInstaller.swift @@ -12,6 +12,6 @@ public protocol IOSListenerForFirmwareInstaller { func logMessageAdvertisement(_ message: String, _ category: String, _ level: String) func stateChangedAdvertisement(_ oldState: EIOSFirmwareInstallationState, _ newState: EIOSFirmwareInstallationState) func busyStateChangedAdvertisement(_ busyNotIdle: Bool) - func fatalErrorOccurredAdvertisement(_ currentState: EIOSFirmwareInstallationState, _ fatalErrorType: EIOSFirmwareInstallerFatalErrorType, _ errorMessage: String) + func fatalErrorOccurredAdvertisement(_ currentState: EIOSFirmwareInstallationState, _ fatalErrorType: EIOSFirmwareInstallerFatalErrorType, _ errorMessage: String, _ globalErrorCode: Int) func firmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(_ progressPercentage: Int, _ averageThroughput: Float32) } diff --git a/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/McuMgrExceptionHelpers.swift b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/McuMgrExceptionHelpers.swift new file mode 100644 index 00000000..a40af5ea --- /dev/null +++ b/Laerdal.McuMgr.Bindings.MacCatalystAndIos.Native/McuMgrBindingsiOS/McuMgrBindingsiOS/McuMgrExceptionHelpers.swift @@ -0,0 +1,28 @@ +import Foundation +import iOSMcuManagerLibrary + +internal class McuMgrExceptionHelpers { + static func deduceGlobalErrorCodeFromException(_ error: Error? = nil) -> Int { //00 + guard let mcuMgrError = error as? McuMgrError else { + return -99 + } + + var errorCode = -99 + var groupErrorCode = -99 + var groupSubsystemId = 0 + + switch mcuMgrError { + case .returnCode(let rc): //if smp v2 is not supported by the target device (rare these days) + errorCode = Int(rc.rawValue) + case .groupCode(let groupCode): //if smp v2 is supported by the target device + groupErrorCode = Int(groupCode.rc.rawValue) + groupSubsystemId = Int(groupCode.group) + } + + return groupSubsystemId == 0 + ? errorCode + : ((groupSubsystemId + 1) * 1000 + groupErrorCode); + + //00 https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-Manager/issues/198 + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj b/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj index 85e8d8c6..f3e3ca9b 100644 --- a/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj +++ b/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj @@ -2,10 +2,10 @@ - true - true - true - true + true + true + true + true netstandard2.1 @@ -37,10 +37,10 @@ $(AllowedReferenceRelatedFileExtensions);.pdb - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 $(PackageId) McuMgr C# Implementation (WIP) diff --git a/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj b/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj index cf5fabcd..87aa794c 100644 --- a/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj +++ b/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj @@ -2,15 +2,15 @@ - $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)) ) - $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)) ) - $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)) ) - true + $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)) ) + $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)) ) + $( [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)) ) + true - $(TargetFrameworks)net8.0-ios; - $(TargetFrameworks)netstandard2.1; + $(TargetFrameworks)net8.0-ios; + $(TargetFrameworks)netstandard2.1; true @@ -18,9 +18,12 @@ true true - true + true - 11.0 + + $(Laerdal_Bindings_iOS___DotnetTargetPlatformVersion) + 17.0 + 11.0 bin\ Library @@ -36,7 +39,7 @@ $(NativeFrameworkParentFolderpath)/McuMgrBindingsiOS.framework - + @@ -70,10 +73,10 @@ $(AllowedReferenceRelatedFileExtensions);.pdb - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 $(PackageId) McuMgr Bindings for iOS - MAUI ready @@ -110,7 +113,7 @@ - + @@ -138,7 +141,9 @@ - + + + @@ -165,6 +170,7 @@ + <_CliCommand>$(_CliCommand) SUPPORTS_MACCATALYST='NO' <_CliCommand>$(_CliCommand) SWIFT_OUTPUT_PATH='$(NativeFrameworkParentFolderpath)' <_CliCommand>$(_CliCommand) XCODE_IDE_DEV_PATH='$(Laerdal_Bindings_iOS___Xcode_Ide_Dev_Path)' <_CliCommand>$(_CliCommand) XCODEBUILD_TARGET_SDK='iphoneos' @@ -218,7 +224,7 @@ - + diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldCompleteSuccessfully_GivenGreenNativeDeviceResetter.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldCompleteSuccessfully_GivenGreenNativeDeviceResetter.cs index d7faf17b..6ccb5834 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldCompleteSuccessfully_GivenGreenNativeDeviceResetter.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldCompleteSuccessfully_GivenGreenNativeDeviceResetter.cs @@ -37,7 +37,7 @@ public MockedGreenNativeDeviceResetterProxySpy1(INativeDeviceResetterCallbacksPr { } - public override void BeginReset() + public override EDeviceResetterInitializationVerdict BeginReset() { base.BeginReset(); @@ -49,6 +49,8 @@ public override void BeginReset() await Task.Delay(20); StateChangedAdvertisement(oldState: EDeviceResetterState.Resetting, newState: EDeviceResetterState.Complete); }); + + return EDeviceResetterInitializationVerdict.Success; //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native resetter } diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs index 54463534..d3bf3efd 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.DeviceResetter.Contracts.Enums; using Laerdal.McuMgr.DeviceResetter.Contracts.Events; @@ -48,7 +49,7 @@ public MockedErroneousDueToBluetoothNativeDeviceResetterProxySpy(INativeDeviceRe { } - public override void BeginReset() + public override EDeviceResetterInitializationVerdict BeginReset() { base.BeginReset(); @@ -59,10 +60,12 @@ public override void BeginReset() await Task.Delay(20); StateChangedAdvertisement(oldState: EDeviceResetterState.Resetting, newState: EDeviceResetterState.Failed); - FatalErrorOccurredAdvertisement("bluetooth error blah blah"); + FatalErrorOccurredAdvertisement("bluetooth error blah blah", EGlobalErrorCode.Generic); //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native resetter }); + + return EDeviceResetterInitializationVerdict.Success; } } } diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs index ac26dd25..41bb8f18 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs @@ -44,7 +44,7 @@ public MockedErroneousDueToMissingSymbolsNativeDeviceResetterProxySpy(INativeDev { } - public override void BeginReset() + public override EDeviceResetterInitializationVerdict BeginReset() { base.BeginReset(); diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs index 824b34a7..bb1e4bc5 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs @@ -48,7 +48,7 @@ public MockedGreenButSlowNativeDeviceResetterProxySpy(INativeDeviceResetterCallb { } - public override void BeginReset() + public override EDeviceResetterInitializationVerdict BeginReset() { base.BeginReset(); @@ -60,6 +60,8 @@ public override void BeginReset() await Task.Delay(1_000); StateChangedAdvertisement(oldState: EDeviceResetterState.Resetting, newState: EDeviceResetterState.Complete); }); + + return EDeviceResetterInitializationVerdict.Success; //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native resetter } diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.cs index 7dd9efe6..6483e3de 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.cs @@ -29,9 +29,11 @@ protected MockedNativeDeviceResetterProxySpy(INativeDeviceResetterCallbacksProxy _resetterCallbacksProxy = resetterCallbacksProxy; } - public virtual void BeginReset() + public virtual EDeviceResetterInitializationVerdict BeginReset() { BeginResetCalled = true; + + return EDeviceResetterInitializationVerdict.Success; } public virtual void Disconnect() @@ -40,16 +42,16 @@ public virtual void Disconnect() } public void LogMessageAdvertisement(string message, string category, ELogLevel level) - => _resetterCallbacksProxy.LogMessageAdvertisement(message, category, level); //raises the actual event + => _resetterCallbacksProxy?.LogMessageAdvertisement(message, category, level); //raises the actual event public void StateChangedAdvertisement(EDeviceResetterState oldState, EDeviceResetterState newState) { State = newState; - _resetterCallbacksProxy.StateChangedAdvertisement(newState: newState, oldState: oldState); //raises the actual event + _resetterCallbacksProxy?.StateChangedAdvertisement(newState: newState, oldState: oldState); //raises the actual event } - public void FatalErrorOccurredAdvertisement(string errorMessage) - => _resetterCallbacksProxy.FatalErrorOccurredAdvertisement(errorMessage); //raises the actual event + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) + => _resetterCallbacksProxy?.FatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); //raises the actual event } } } \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.BeginDownload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.BeginDownload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs index d7360630..664f54ba 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.BeginDownload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.BeginDownload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs @@ -25,7 +25,12 @@ public void BeginDownload_ShouldThrowArgumentException_GivenInvalidRemoteFilePat using var eventsMonitor = fileDownloader.Monitor(); // Act - var work = new Func(() => fileDownloader.BeginDownload(remoteFilePath: remoteFilePath)); + var work = new Func(() => fileDownloader.BeginDownload( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePath: remoteFilePath + )); // Assert work.Should().ThrowExactly(); @@ -49,9 +54,12 @@ public MockedGreenNativeFileDownloaderProxySpy1(INativeFileDownloaderCallbacksPr _mockedFileData = mockedFileData; } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs index b88a9a36..f303e8b4 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs @@ -20,7 +20,12 @@ public async Task MultipleFilesDownloadAsync_ShouldCompleteSuccessfully_GivenNoF using var eventsMonitor = fileDownloader.Monitor(); // Act - var work = new Func>>(async () => await fileDownloader.DownloadAsync(Enumerable.Empty())); + var work = new Func>>(async () => await fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePaths: [] + )); // Assert var results = (await work.Should().CompleteWithinAsync(500.Milliseconds())).Which; @@ -41,9 +46,12 @@ public MockedGreenNativeFileDownloaderProxySpy5(INativeFileDownloaderCallbacksPr { } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -52,7 +60,7 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) await Task.Delay(20); StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Complete); // order - DownloadCompletedAdvertisement(remoteFilePath, new byte[] { }); // order + DownloadCompletedAdvertisement(remoteFilePath, []); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs index e6046e37..25a8aeb7 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs @@ -1,7 +1,9 @@ using FluentAssertions; using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; using GenericNativeFileDownloaderCallbacksProxy_ = Laerdal.McuMgr.FileDownloader.FileDownloader.GenericNativeFileDownloaderCallbacksProxy; #pragma warning disable xUnit1026 @@ -16,11 +18,13 @@ public async Task MultipleFilesDownloadAsync_ShouldCompleteSuccessfully_GivenVar // Arrange var expectedResults = new Dictionary { - { "/some/file/that/exists.bin", new byte[] { 1 } }, - { "/Some/File/That/Exists.bin", new byte[] { 2 } }, + { "/some/file/that/exists.bin", [1] }, + { "/Some/File/That/Exists.bin", [2] }, { "/some/file/that/doesnt/exist.bin", null }, - { "/some/file/that/exist/and/completes/after/a/couple/of/attempts.bin", new byte[] { 3 } }, + { "/some/file/that/exist/and/completes/after/a/couple/of/attempts.bin", [3] }, { "/some/file/that/exist/but/is/erroring/out/when/we/try/to/download/it.bin", null }, + { "/some/file/path/pointing/to/a/Directory", null }, + { "/some/file/path/pointing/to/a/directory", null }, }; var mockedNativeFileDownloaderProxy = new MockedGreenNativeFileDownloaderProxySpy6(new GenericNativeFileDownloaderCallbacksProxy_(), expectedResults); var fileDownloader = new McuMgr.FileDownloader.FileDownloader(mockedNativeFileDownloaderProxy); @@ -40,18 +44,23 @@ public async Task MultipleFilesDownloadAsync_ShouldCompleteSuccessfully_GivenVar "/some/file/that/exist/and/completes/after/a/couple/of/attempts.bin", //intentionally included multiple times to test whether the mechanism will attempt to download the file only once "/some/file/that/exist/but/is/erroring/out/when/we/try/to/download/it.bin", "/some/file/that/exist/but/is/erroring/out/when/we/try/to/download/it.bin", //intentionally included multiple times to test whether the mechanism will attempt to download the file only once + "some/file/path/pointing/to/a/Directory", + "/some/file/path/pointing/to/a/directory", }; using var eventsMonitor = fileDownloader.Monitor(); // Act var work = new Func>>(async () => await fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + remoteFilePaths: remoteFilePathsToTest, maxRetriesPerDownload: 4 )); // Assert - var results = (await work.Should().CompleteWithinAsync(5.Seconds())).Which; + var results = (await work.Should().CompleteWithinAsync(20.Seconds())).Which; results.Should().BeEquivalentTo(expectedResults); @@ -63,7 +72,7 @@ public async Task MultipleFilesDownloadAsync_ShouldCompleteSuccessfully_GivenVar eventsMonitor.OccurredEvents .Count(args => args.EventName == nameof(fileDownloader.FatalErrorOccurred)) .Should() - .Be(8); + .Be(10); mockedNativeFileDownloaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 @@ -82,9 +91,12 @@ public MockedGreenNativeFileDownloaderProxySpy6(INativeFileDownloaderCallbacksPr } private int _retryCountForProblematicFile; - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -97,23 +109,28 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) if (remoteFilePathUppercase.Contains("some/file/that/exist/but/is/erroring/out/when/we/try/to/download/it.bin".ToUpperInvariant())) { StateChangedAdvertisement(remoteFilePath, oldState: EFileDownloaderState.Downloading, newState: EFileDownloaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "foobar"); + FatalErrorOccurredAdvertisement(remoteFilePath, "foobar", EGlobalErrorCode.Unset); } else if (remoteFilePathUppercase.Contains("some/file/that/doesnt/exist.bin".ToUpperInvariant())) { StateChangedAdvertisement(remoteFilePath, oldState: EFileDownloaderState.Downloading, newState: EFileDownloaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "NO ENTRY (5)"); + FatalErrorOccurredAdvertisement(remoteFilePath, "IN VALUE (3)", EGlobalErrorCode.SubSystemFilesystem_NotFound); } else if (remoteFilePathUppercase.Contains("some/file/that/exist/and/completes/after/a/couple/of/attempts.bin".ToUpperInvariant()) && _retryCountForProblematicFile++ < 3) { StateChangedAdvertisement(remoteFilePath, oldState: EFileDownloaderState.Downloading, newState: EFileDownloaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "ping pong"); + FatalErrorOccurredAdvertisement(remoteFilePath, "ping pong", EGlobalErrorCode.McuMgrErrorBeforeSmpV2_Corrupt); + } + else if (remoteFilePathUppercase.Contains("some/file/path/pointing/to/a/directory".ToUpperInvariant())) + { + StateChangedAdvertisement(remoteFilePath, oldState: EFileDownloaderState.Downloading, newState: EFileDownloaderState.Error); + FatalErrorOccurredAdvertisement(remoteFilePath, "BLAH BLAH (4)", EGlobalErrorCode.SubSystemFilesystem_IsDirectory); } else { _expectedResults.TryGetValue(remoteFilePath, out var expectedFileContent); - + StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Complete); // order DownloadCompletedAdvertisement(remoteFilePath, expectedFileContent); // order } diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs index 8981bd7a..a764b37f 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs @@ -29,7 +29,12 @@ public async Task MultipleFilesDownloadAsync_ShouldThrowArgumentException_GivenP using var eventsMonitor = fileDownloader.Monitor(); // Act - var work = new Func>>(async () => await fileDownloader.DownloadAsync(remoteFilePaths)); + var work = new Func>>(async () => await fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePaths: remoteFilePaths + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); @@ -49,9 +54,12 @@ public MockedGreenNativeFileDownloaderProxySpy11(INativeFileDownloaderCallbacksP { } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -61,7 +69,7 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) await Task.Delay(20); StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Complete); // order - DownloadCompletedAdvertisement(remoteFilePath, new byte[] { }); // order + DownloadCompletedAdvertisement(remoteFilePath, []); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs index b0525551..bc9f6686 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs @@ -20,7 +20,12 @@ public async Task MultipleFilesDownloadAsync_ShouldThrowNullArgumentException_Gi using var eventsMonitor = fileDownloader.Monitor(); // Act - var work = new Func>>(async () => await fileDownloader.DownloadAsync(remoteFilePaths: null)); + var work = new Func>>(async () => await fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePaths: null + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); @@ -40,9 +45,12 @@ public MockedGreenNativeFileDownloaderProxySpy10(INativeFileDownloaderCallbacksP { } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -52,7 +60,7 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) await Task.Delay(20); StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Complete); // order - DownloadCompletedAdvertisement(remoteFilePath, new byte[] { }); // order + DownloadCompletedAdvertisement(remoteFilePath, []); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyConnection.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyConnection.cs new file mode 100644 index 00000000..38a1d9e3 --- /dev/null +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyConnection.cs @@ -0,0 +1,165 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Constants; +using Laerdal.McuMgr.Common.Enums; +using Laerdal.McuMgr.Common.Events; +using Laerdal.McuMgr.FileDownloader.Contracts.Enums; +using Laerdal.McuMgr.FileDownloader.Contracts.Events; +using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; +using GenericNativeFileDownloaderCallbacksProxy_ = Laerdal.McuMgr.FileDownloader.FileDownloader.GenericNativeFileDownloaderCallbacksProxy; + +#pragma warning disable xUnit1026 + +namespace Laerdal.McuMgr.Tests.FileDownloader +{ + public partial class FileDownloaderTestbed + { + [Theory] + [InlineData("FDT.SFDA.SCSBFBTFS.GFBC.010", "/path/to/file.bin", 2)] + // [InlineData("FDT.SFDA.SCSBFBTFS.GFBC.020", "/path/to/file.bin", 3)] + // [InlineData("FDT.SFDA.SCSBFBTFS.GFBC.030", "/path/to/file.bin", 5)] + public async Task SingleFileDownloadAsync_ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyBluetoothConnection(string testcaseNickname, string remoteFilePath, int maxTriesCount) + { + // Arrange + var expectedData = (byte[]) [1, 2, 3]; + + var mockedNativeFileDownloaderProxy = new MockedGreenNativeFileDownloaderProxySpy120( + expectedData: expectedData, + maxTriesCount: maxTriesCount, + uploaderCallbacksProxy: new GenericNativeFileDownloaderCallbacksProxy_() + ); + var fileDownloader = new McuMgr.FileDownloader.FileDownloader(mockedNativeFileDownloaderProxy); + + using var eventsMonitor = fileDownloader.Monitor(); + + // Act + var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + maxTriesCount: maxTriesCount, + remoteFilePath: remoteFilePath + )); + + // Assert + await work.Should().CompleteWithinAsync((maxTriesCount * 200).Seconds()); + + mockedNativeFileDownloaderProxy.BugDetected.Should().BeNull(); + mockedNativeFileDownloaderProxy.CancelCalled.Should().BeFalse(); + mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 + mockedNativeFileDownloaderProxy.BeginDownloadCalled.Should().BeTrue(); + + eventsMonitor + .OccurredEvents.Where(x => x.EventName == nameof(fileDownloader.FatalErrorOccurred)) + .Should().HaveCount(maxTriesCount - 1); //one error for each try except the last one + + eventsMonitor + .Should().Raise(nameof(fileDownloader.StateChanged)) + .WithSender(fileDownloader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileDownloaderState.Downloading); + + eventsMonitor + .OccurredEvents + .Where(x => x.EventName == nameof(fileDownloader.LogEmitted)) + .SelectMany(x => x.Parameters) + .OfType() + .Count(l => l is { Level: ELogLevel.Warning } && l.Message.Contains("[FD.DA.010]")) + .Should() + .Be(1); + + eventsMonitor + .Should().Raise(nameof(fileDownloader.StateChanged)) + .WithSender(fileDownloader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileDownloaderState.Complete); + + eventsMonitor + .Should().Raise(nameof(fileDownloader.DownloadCompleted)) + .WithSender(fileDownloader) + .WithArgs(args => args.Resource == remoteFilePath); + + //00 we dont want to disconnect the device regardless of the outcome + } + + private class MockedGreenNativeFileDownloaderProxySpy120 : FileDownloader.FileDownloaderTestbed.MockedNativeFileDownloaderProxySpy + { + private readonly int _maxTriesCount; + private readonly byte[] _expectedData; + + public string BugDetected { get; private set; } + + public MockedGreenNativeFileDownloaderProxySpy120(byte[] expectedData, INativeFileDownloaderCallbacksProxy uploaderCallbacksProxy, int maxTriesCount) : base(uploaderCallbacksProxy) + { + _expectedData = expectedData; + _maxTriesCount = maxTriesCount; + } + + private int _tryCounter; + public override EFileDownloaderVerdict BeginDownload( + string remoteFilePath, + int? initialMtuSize = null // android only + ) + { + _tryCounter++; + + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize // android only + ); + + Task.Run(async () => //00 vital + { + await Task.Delay(10); + StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Idle, EFileDownloaderState.Downloading); + + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(00, 00); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(10, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(20, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(30, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(40, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(50, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(60, 10); + + if (_tryCounter == _maxTriesCount && initialMtuSize != AndroidTidbits.BleConnectionFailsafeSettings.ForDownloading.InitialMtuSize) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(initialMtuSize)} set to {AndroidTidbits.BleConnectionFailsafeSettings.ForDownloading.InitialMtuSize} but it's set to {initialMtuSize?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, BugDetected, EGlobalErrorCode.Generic); // order + return; + } + + if (_tryCounter < _maxTriesCount) + { + await Task.Delay(20); + StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EGlobalErrorCode.Generic); // order + return; + } + + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(70, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(80, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(90, 10); + await Task.Delay(5); + FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(100, 10); + + StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Complete); // order + DownloadCompletedAdvertisement(remoteFilePath, _expectedData); // order + }); + + return verdict; + + //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native uploader + } + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs index be38c0c6..6abece2b 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs @@ -1,8 +1,10 @@ using FluentAssertions; using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Events; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; using GenericNativeFileDownloaderCallbacksProxy_ = Laerdal.McuMgr.FileDownloader.FileDownloader.GenericNativeFileDownloaderCallbacksProxy; #pragma warning disable xUnit1026 @@ -23,7 +25,7 @@ public async Task SingleFileDownloadAsync_ShouldCompleteSuccessfully_GivenGreenN { // Arrange var mockedFileData = new byte[] { 1, 2, 3 }; - var expectedRemoteFilepath = remoteFilePath.StartsWith("/") + var expectedRemoteFilepath = remoteFilePath.StartsWith('/') ? remoteFilePath : $"/{remoteFilePath}"; @@ -38,6 +40,9 @@ public async Task SingleFileDownloadAsync_ShouldCompleteSuccessfully_GivenGreenN // Act var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + maxTriesCount: maxTriesCount, remoteFilePath: remoteFilePath, sleepTimeBetweenRetriesInMs: sleepTimeBetweenRetriesInMs @@ -84,11 +89,14 @@ public MockedGreenNativeFileDownloaderProxySpy(INativeFileDownloaderCallbacksPro } private int _tryCount; - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { _tryCount++; - - var verdict = base.BeginDownload(remoteFilePath); + + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -99,7 +107,7 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) if (_tryCount < _maxNumberOfTriesForSuccess) { StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred"); + FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EGlobalErrorCode.McuMgrErrorBeforeSmpV2_Corrupt); return; } diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs index 02a66276..1139edb6 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs @@ -1,11 +1,13 @@ using System.Diagnostics.CodeAnalysis; using FluentAssertions; using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Events; using Laerdal.McuMgr.FileDownloader.Contracts.Exceptions; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; using GenericNativeFileDownloaderCallbacksProxy_ = Laerdal.McuMgr.FileDownloader.FileDownloader.GenericNativeFileDownloaderCallbacksProxy; namespace Laerdal.McuMgr.Tests.FileDownloader @@ -29,6 +31,9 @@ public async Task SingleFileDownloadAsync_ShouldThrowAllDownloadAttemptsFailedEx // Act var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + maxTriesCount: maxTriesCount, remoteFilePath: remoteFilePath )); @@ -71,9 +76,12 @@ public MockedGreenNativeFileDownloaderProxySpy4(INativeFileDownloaderCallbacksPr _ = mockedFileData; } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -84,7 +92,7 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) await Task.Delay(2_000); StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred"); + FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EGlobalErrorCode.Generic); }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs index ecf07e72..2f33cb71 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs @@ -1,11 +1,13 @@ using System.Diagnostics.CodeAnalysis; using FluentAssertions; using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Events; using Laerdal.McuMgr.FileDownloader.Contracts.Exceptions; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; using GenericNativeFileDownloaderCallbacksProxy_ = Laerdal.McuMgr.FileDownloader.FileDownloader.GenericNativeFileDownloaderCallbacksProxy; namespace Laerdal.McuMgr.Tests.FileDownloader @@ -25,7 +27,7 @@ public async Task SingleFileDownloadAsync_ShouldThrowAllDownloadAttemptsFailedEx var mockedNativeFileDownloaderProxy = new MockedErroneousNativeFileDownloaderProxySpy13( mockedFileData: mockedFileData, downloaderCallbacksProxy: new GenericNativeFileDownloaderCallbacksProxy_(), - nativeErrorMessageForFileNotFound: nativeRogueErrorMessage + rogueNativeErrorMessage: nativeRogueErrorMessage ); var fileDownloader = new McuMgr.FileDownloader.FileDownloader(mockedNativeFileDownloaderProxy); @@ -33,6 +35,9 @@ public async Task SingleFileDownloadAsync_ShouldThrowAllDownloadAttemptsFailedEx // Act var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + maxTriesCount: maxTriesCount, //doesnt really matter we just want to ensure that the method fails early and doesnt retry remoteFilePath: remoteFilePath, sleepTimeBetweenRetriesInMs: 10 @@ -74,17 +79,20 @@ await work.Should() private class MockedErroneousNativeFileDownloaderProxySpy13 : MockedNativeFileDownloaderProxySpy { - private readonly string _nativeErrorMessageForFileNotFound; + private readonly string _rogueNativeErrorMessage; - public MockedErroneousNativeFileDownloaderProxySpy13(INativeFileDownloaderCallbacksProxy downloaderCallbacksProxy, byte[] mockedFileData, string nativeErrorMessageForFileNotFound) : base(downloaderCallbacksProxy) + public MockedErroneousNativeFileDownloaderProxySpy13(INativeFileDownloaderCallbacksProxy downloaderCallbacksProxy, byte[] mockedFileData, string rogueNativeErrorMessage) : base(downloaderCallbacksProxy) { _ = mockedFileData; - _nativeErrorMessageForFileNotFound = nativeErrorMessageForFileNotFound; + _rogueNativeErrorMessage = rogueNativeErrorMessage; } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -95,7 +103,7 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) await Task.Delay(100); StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, _nativeErrorMessageForFileNotFound); + FatalErrorOccurredAdvertisement(remoteFilePath, _rogueNativeErrorMessage, EGlobalErrorCode.Generic); }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs index 6c5ddf2f..5a2ef7a1 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs @@ -21,7 +21,12 @@ public async Task SingleFileDownloadAsync_ShouldThrowArgumentException_GivenEmpt using var eventsMonitor = fileDownloader.Monitor(); // Act - var work = new Func(() => fileDownloader.DownloadAsync(remoteFilePath: remoteFilePath)); + var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePath: remoteFilePath + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); @@ -45,9 +50,12 @@ public MockedGreenNativeFileDownloaderProxySpy2(INativeFileDownloaderCallbacksPr _mockedFileData = mockedFileData; } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs index 8da54ee3..216db887 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs @@ -33,7 +33,12 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadCancelledException_Give fileDownloader.Cancel(); }); - var work = new Func(() => fileDownloader.DownloadAsync(remoteFilePath)); + var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePath: remoteFilePath + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); @@ -91,7 +96,7 @@ public override void Cancel() // a best effort basis and this is exactly what we are testing here } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { _currentRemoteFilePath = remoteFilePath; _cancellationTokenSource = new CancellationTokenSource(); @@ -100,8 +105,11 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) { _cancellationTokenSource.Cancel(); }; - - var verdict = base.BeginDownload(remoteFilePath); + + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadInternalErrorException_GivenErroneousNativeFileDownloader.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadInternalErrorException_GivenErroneousNativeFileDownloader.cs index 87a192a6..2a5e8be1 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadInternalErrorException_GivenErroneousNativeFileDownloader.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadInternalErrorException_GivenErroneousNativeFileDownloader.cs @@ -16,7 +16,12 @@ public async Task SingleFileDownloadAsync_ShouldThrowDownloadInternalErrorExcept var fileDownloader = new McuMgr.FileDownloader.FileDownloader(mockedNativeFileDownloaderProxy); // Act - var work = new Func(() => fileDownloader.DownloadAsync(remoteFilePath: "/path/to/file.bin")); + var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePath: "/path/to/file.bin" + )); // Assert (await work.Should().ThrowExactlyAsync()).WithInnerExceptionExactly("native symbols not loaded blah blah"); @@ -34,9 +39,12 @@ public MockedErroneousNativeFileDownloaderProxySpy(INativeFileDownloaderCallback { } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - base.BeginDownload(remoteFilePath); + base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Thread.Sleep(100); diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs index 01d543ad..1752b3c1 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs @@ -23,7 +23,13 @@ public async Task SingleFileDownloadAsync_ShouldThrowDownloadTimeoutException_Gi using var eventsMonitor = fileDownloader.Monitor(); // Act - var work = new Func(() => fileDownloader.DownloadAsync(remoteFilePath: remoteFilePath, timeoutForDownloadInMs: 100)); + var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePath: remoteFilePath, + timeoutForDownloadInMs: 100 + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); @@ -51,9 +57,12 @@ public MockedGreenButSlowNativeFileDownloaderProxySpy(INativeFileDownloaderCallb { } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -64,7 +73,7 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) await Task.Delay(1_000); StateChangedAdvertisement(resource: remoteFilePath, oldState: EFileDownloaderState.Downloading, newState: EFileDownloaderState.Complete); // order - DownloadCompletedAdvertisement(remoteFilePath, new byte[]{}); // order + DownloadCompletedAdvertisement(remoteFilePath, []); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs index cba86ed4..1eb9e6b4 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs @@ -1,11 +1,13 @@ using System.Diagnostics.CodeAnalysis; using FluentAssertions; using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Events; using Laerdal.McuMgr.FileDownloader.Contracts.Exceptions; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; using GenericNativeFileDownloaderCallbacksProxy_ = Laerdal.McuMgr.FileDownloader.FileDownloader.GenericNativeFileDownloaderCallbacksProxy; namespace Laerdal.McuMgr.Tests.FileDownloader @@ -14,11 +16,11 @@ namespace Laerdal.McuMgr.Tests.FileDownloader public partial class FileDownloaderTestbed { [Theory] - [InlineData("FDT.SFDA.STRFNFE.GNEF.010", "NO ENTRY (5)", 1)] //android - [InlineData("FDT.SFDA.STRFNFE.GNEF.020", "NO ENTRY (5)", 2)] //android - [InlineData("FDT.SFDA.STRFNFE.GNEF.030", "NO ENTRY (5)", 3)] //android - [InlineData("FDT.SFDA.STRFNFE.GNEF.040", "NO_ENTRY (5)", 2)] //ios - public async Task SingleFileDownloadAsync_ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath(string testcaseNickname, string nativeErrorMessageForFileNotFound, int maxTriesCount) + [InlineData("FDT.SFDA.STRFNFE.GNEF.010", 1)] //android + [InlineData("FDT.SFDA.STRFNFE.GNEF.020", 2)] //android + [InlineData("FDT.SFDA.STRFNFE.GNEF.030", 3)] //android + [InlineData("FDT.SFDA.STRFNFE.GNEF.040", 2)] //ios + public async Task SingleFileDownloadAsync_ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath(string testcaseNickname, int maxTriesCount) { // Arrange var mockedFileData = new byte[] { 1, 2, 3 }; @@ -26,8 +28,7 @@ public async Task SingleFileDownloadAsync_ShouldThrowRemoteFileNotFoundException var mockedNativeFileDownloaderProxy = new MockedErroneousNativeFileDownloaderProxySpy2( mockedFileData: mockedFileData, - downloaderCallbacksProxy: new GenericNativeFileDownloaderCallbacksProxy_(), - nativeErrorMessageForFileNotFound: nativeErrorMessageForFileNotFound + downloaderCallbacksProxy: new GenericNativeFileDownloaderCallbacksProxy_() ); var fileDownloader = new McuMgr.FileDownloader.FileDownloader(mockedNativeFileDownloaderProxy); @@ -35,6 +36,9 @@ public async Task SingleFileDownloadAsync_ShouldThrowRemoteFileNotFoundException // Act var work = new Func(() => fileDownloader.DownloadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + maxTriesCount: maxTriesCount, //doesnt really matter we just want to ensure that the method fails early and doesnt retry remoteFilePath: remoteFilePath, sleepTimeBetweenRetriesInMs: 10 @@ -60,7 +64,7 @@ await work.Should() eventsMonitor .Should().Raise(nameof(fileDownloader.FatalErrorOccurred)) .WithSender(fileDownloader) - .WithArgs(args => args.ErrorMessage.ToUpperInvariant().Contains(nativeErrorMessageForFileNotFound.ToUpperInvariant())); + .WithArgs(args => args.GlobalErrorCode == EGlobalErrorCode.SubSystemFilesystem_NotFound); eventsMonitor .Should().Raise(nameof(fileDownloader.StateChanged)) @@ -77,17 +81,17 @@ await work.Should() private class MockedErroneousNativeFileDownloaderProxySpy2 : MockedNativeFileDownloaderProxySpy { - private readonly string _nativeErrorMessageForFileNotFound; - - public MockedErroneousNativeFileDownloaderProxySpy2(INativeFileDownloaderCallbacksProxy downloaderCallbacksProxy, byte[] mockedFileData, string nativeErrorMessageForFileNotFound) : base(downloaderCallbacksProxy) + public MockedErroneousNativeFileDownloaderProxySpy2(INativeFileDownloaderCallbacksProxy downloaderCallbacksProxy, byte[] mockedFileData) : base(downloaderCallbacksProxy) { _ = mockedFileData; - _nativeErrorMessageForFileNotFound = nativeErrorMessageForFileNotFound; } - public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public override EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null) { - var verdict = base.BeginDownload(remoteFilePath); + var verdict = base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); Task.Run(async () => //00 vital { @@ -97,8 +101,8 @@ public override EFileDownloaderVerdict BeginDownload(string remoteFilePath) await Task.Delay(100); - StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Error); // order simulates how the native code behaves - FatalErrorOccurredAdvertisement(remoteFilePath, _nativeErrorMessageForFileNotFound); // order simulates how the csharp wrapper behaves + StateChangedAdvertisement(remoteFilePath, EFileDownloaderState.Downloading, EFileDownloaderState.Error); // order simulates how the native code behaves + FatalErrorOccurredAdvertisement(remoteFilePath, "foobar", EGlobalErrorCode.SubSystemFilesystem_NotFound); // order simulates how the csharp wrapper behaves }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.cs index a4f9c319..9e7a2c08 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.cs @@ -2,6 +2,7 @@ using Laerdal.McuMgr.FileDownloader.Contracts; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; namespace Laerdal.McuMgr.Tests.FileDownloader { @@ -28,7 +29,10 @@ protected MockedNativeFileDownloaderProxySpy(INativeFileDownloaderCallbacksProxy _downloaderCallbacksProxy = downloaderCallbacksProxy; } - public virtual EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public virtual EFileDownloaderVerdict BeginDownload( + string remoteFilePath, + int? initialMtuSize = null + ) { BeginDownloadCalled = true; @@ -59,13 +63,17 @@ public void BusyStateChangedAdvertisement(bool busyNotIdle) public void DownloadCompletedAdvertisement(string resource, byte[] data) => _downloaderCallbacksProxy.DownloadCompletedAdvertisement(resource, data); //raises the actual event - - public void FatalErrorOccurredAdvertisement(string resource, string errorMessage) - => _downloaderCallbacksProxy.FatalErrorOccurredAdvertisement(resource, errorMessage); //raises the actual event + + public void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EGlobalErrorCode globalErrorCode) + => _downloaderCallbacksProxy.FatalErrorOccurredAdvertisement(resource, errorMessage, globalErrorCode); //raises the actual event public void FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput) => _downloaderCallbacksProxy.FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage, averageThroughput); //raises the actual event + public bool TrySetContext(object context) => throw new NotImplementedException(); + public bool TrySetBluetoothDevice(object bluetoothDevice) => throw new NotImplementedException(); + public bool TryInvalidateCachedTransport() => throw new NotImplementedException(); + public void Dispose() { } diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.BeginUpload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.BeginUpload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs index c9398157..55c8971e 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.BeginUpload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.BeginUpload.ShouldThrowArgumentException_GivenInvalidRemoteFilePath.cs @@ -25,7 +25,13 @@ public void BeginUpload_ShouldThrowArgumentException_GivenInvalidRemoteFilePath( using var eventsMonitor = fileUploader.Monitor(); // Act - var work = new Func(() => fileUploader.BeginUpload(remoteFilePath, mockedFileData)); + var work = new Func(() => fileUploader.BeginUpload( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: mockedFileData, + remoteFilePath: remoteFilePath + )); // Assert work.Should().ThrowExactly(); @@ -46,9 +52,27 @@ public MockedGreenNativeFileUploaderProxySpy1(INativeFileUploaderCallbacksProxy { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] mockedFileData) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, mockedFileData); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs index a12c4c65..1882f7a9 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenNoFilesToDownload.cs @@ -20,7 +20,11 @@ public async Task MultipleFilesUploadAsync_ShouldCompleteSuccessfully_GivenNoFil using var eventsMonitor = fileUploader.Monitor(); // Act - var work = new Func(async () => await fileUploader.UploadAsync(new Dictionary>(0))); + var work = new Func(async () => await fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + remoteFilePathsAndTheirData: new Dictionary>(0) + )); // Assert await work.Should().CompleteWithinAsync(500.Milliseconds()); @@ -40,9 +44,27 @@ public MockedGreenNativeFileUploaderProxySpy5(INativeFileUploaderCallbacksProxy { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs index 438df8a2..19852ec0 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldCompleteSuccessfully_GivenVariousFilesToDownload.cs @@ -26,33 +26,39 @@ public async Task MultipleFilesUploadAsync_ShouldCompleteSuccessfully_GivenVario var remoteFilePathsToTest = new Dictionary { - { "\r some/file/path.bin ", new byte[] { 0 } }, - { " /some/file/path.bin ", new byte[] { 0 } }, - { "\t/some/file/path.bin ", new byte[] { 0 } }, - { " some/file/path.bin ", new byte[] { 1 } }, //intentionally included multiple times to test whether the mechanism will attempt to upload the file only once - { " Some/File/Path.bin ", new byte[] { 0 } }, - { "\t/Some/File/Path.bin ", new byte[] { 0 } }, - { " /Some/File/Path.bin ", new byte[] { 1 } }, //intentionally included multiple times to test that we handle case sensitivity correctly - { "\t/some/file/that/succeeds/after/a/couple/of/attempts.bin ", new byte[] { 0 } }, - { " /some/file/that/succeeds/after/a/couple/of/attempts.bin ", new byte[] { 1 } }, //intentionally included multiple times to test whether the mechanism will attempt to upload the file only once - - { " /some/file/to/a/folder/that/doesnt/exist.bin ", new byte[] { 0 } }, - { "\n some/file/that/is/erroring/out/when/we/try/to/upload/it.bin ", new byte[] { 0 } }, - { "\r/some/file/that/is/erroring/out/when/we/try/to/upload/it.bin ", new byte[] { 1 } }, //intentionally included multiple times to test whether the mechanism will attempt to upload the file only once + { "\r some/file/path.bin ", [0] }, + { " /some/file/path.bin ", [0] }, + { "\t/some/file/path.bin ", [0] }, + { " some/file/path.bin ", [1] }, //intentionally included multiple times to test whether the mechanism will attempt to upload the file only once + { " Some/File/Path.bin ", [0] }, + { "\t/Some/File/Path.bin ", [0] }, + { " /Some/File/Path.bin ", [1] }, //intentionally included multiple times to test that we handle case sensitivity correctly + { "\t/some/file/that/succeeds/after/a/couple/of/attempts.bin ", [0] }, + { " /some/file/that/succeeds/after/a/couple/of/attempts.bin ", [1] }, //intentionally included multiple times to test whether the mechanism will attempt to upload the file only once + + { " /some/file/to/a/folder/that/doesnt/exist.bin ", [0] }, + { "\n some/file/that/is/erroring/out/when/we/try/to/upload/it.bin ", [0] }, + { "\r/some/file/that/is/erroring/out/when/we/try/to/upload/it.bin ", [1] }, //intentionally included multiple times to test whether the mechanism will attempt to upload the file only once }; using var eventsMonitor = fileUploader.Monitor(); // Act - var work = new Func>>(async () => await fileUploader.UploadAsync(remoteFilePathsAndTheirData: remoteFilePathsToTest, maxTriesPerUpload: 4)); + var work = new Func>>(async () => await fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + maxTriesPerUpload: 4, + remoteFilePathsAndTheirData: remoteFilePathsToTest + )); var filesThatFailedToBeUploaded = (await work.Should().CompleteWithinAsync(6.Seconds())).Which; // Assert - filesThatFailedToBeUploaded.Should().BeEquivalentTo(expectation: new[] - { + filesThatFailedToBeUploaded.Should().BeEquivalentTo(expectation: + [ "/some/file/to/a/folder/that/doesnt/exist.bin", "/some/file/that/is/erroring/out/when/we/try/to/upload/it.bin" - }); + ]); eventsMonitor.OccurredEvents .Count(args => args.EventName == nameof(fileUploader.FileUploaded)) @@ -77,9 +83,27 @@ public MockedGreenNativeFileUploaderProxySpy6(INativeFileUploaderCallbacksProxy } private int _retryCountForProblematicFile; - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { @@ -92,18 +116,18 @@ public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] d if (remoteFilePathUppercase.Contains("some/file/to/a/folder/that/doesnt/exist.bin".ToUpperInvariant())) { StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "UNKNOWN (1)", EMcuMgrErrorCode.Unknown, EFileUploaderGroupReturnCode.Unset); + FatalErrorOccurredAdvertisement(remoteFilePath, "FOOBAR (3)", EGlobalErrorCode.SubSystemFilesystem_NotFound); } else if (remoteFilePathUppercase.Contains("some/file/that/succeeds/after/a/couple/of/attempts.bin".ToUpperInvariant()) && _retryCountForProblematicFile++ < 3) { StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "ping pong", EMcuMgrErrorCode.Busy, EFileUploaderGroupReturnCode.Unset); + FatalErrorOccurredAdvertisement(remoteFilePath, "ping pong", EGlobalErrorCode.Generic); } else if (remoteFilePathUppercase.Contains("some/file/that/is/erroring/out/when/we/try/to/upload/it.bin".ToUpperInvariant())) { StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "native symbols not loaded blah blah", EMcuMgrErrorCode.NotSupported, EFileUploaderGroupReturnCode.Unset); + FatalErrorOccurredAdvertisement(remoteFilePath, "native symbols not loaded blah blah", EGlobalErrorCode.Generic); } else { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs index c40c7949..4a12f1c3 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs @@ -29,7 +29,11 @@ public async Task MultipleFilesUploadAsync_ShouldThrowArgumentException_GivenPat using var eventsMonitor = fileUploader.Monitor(); // Act - var work = new Func(async () => await fileUploader.UploadAsync(remoteFilePaths.ToDictionary(x => x, x => new byte[] { 1 }))); + var work = new Func(async () => await fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + remoteFilePathsAndTheirData: remoteFilePaths.ToDictionary(x => x, _ => new byte[] { 1 }) + )); // Assert await work.Should().ThrowAsync().WithTimeoutInMs(500); //dont use throwexactlyasync<> here @@ -49,9 +53,27 @@ public MockedGreenNativeFileUploaderProxySpy11(INativeFileUploaderCallbacksProxy { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs index 6dc97e0f..1dcfda94 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs @@ -20,7 +20,12 @@ public async Task MultipleFilesUploadAsync_ShouldThrowNullArgumentException_Give using var eventsMonitor = fileUploader.Monitor(); // Act - var work = new Func(async () => await fileUploader.UploadAsync(remoteFilePathsAndTheirData: null)); + var work = new Func(async () => await fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + remoteFilePathsAndTheirData: null + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); @@ -40,9 +45,27 @@ public MockedGreenNativeFileUploaderProxySpy10(INativeFileUploaderCallbacksProxy { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettingsRightAway_GivenProblematicDevices.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettingsRightAway_GivenProblematicDevices.cs new file mode 100644 index 00000000..386a1391 --- /dev/null +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettingsRightAway_GivenProblematicDevices.cs @@ -0,0 +1,185 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Constants; +using Laerdal.McuMgr.Common.Enums; +using Laerdal.McuMgr.Common.Events; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; +using Laerdal.McuMgr.FileUploader.Contracts.Events; +using Laerdal.McuMgr.FileUploader.Contracts.Native; +using GenericNativeFileUploaderCallbacksProxy_ = Laerdal.McuMgr.FileUploader.FileUploader.GenericNativeFileUploaderCallbacksProxy; + +#pragma warning disable xUnit1026 + +namespace Laerdal.McuMgr.Tests.FileUploader +{ + public partial class FileUploaderTestbed + { + [Theory] + [InlineData("FUT.SFUA.SCSBFBTFSRA.GPD.010", "sm-x200 ", " samsung ", null, null, null, null, null, null, null, 23, 1, 1)] + [InlineData("FUT.SFUA.SCSBFBTFSRA.GPD.020", " SM-X200 ", " Samsung ", null, null, null, null, null, null, null, 23, 1, 1)] + [InlineData("FUT.SFUA.SCSBFBTFSRA.GPD.030", " iPhone 6 ", " Apple ", null, null, null, null, null, 1, 1, null, null, null)] + [InlineData("FUT.SFUA.SCSBFBTFSRA.GPD.040", " iPhone 6 ", " Apple ", 2, 4, null, null, null, 2, 4, null, null, null)] + [InlineData("FUT.SFUA.SCSBFBTFSRA.GPD.050", " foobar ", " AcmeCorp. ", null, null, null, null, null, null, null, null, null, null)] + public async Task SingleFileUploadAsync_ShouldCompleteSuccessfullyByFallingBackToFailsafeSettingsRightAway_GivenProblematicDevices( + string testcaseNickname, + string hostDeviceModel, + string hostDeviceManufacturer, + + int? pipelineDepth, + int? byteAlignment, + int? initialMtuSize, + int? windowCapacity, + int? memoryAlignment, + + int? expectedPipelineDepth, + int? expectedByteAlignment, + int? expectedInitialMtuSize, + int? expectedWindowCapacity, + int? expectedMemoryAlignment + ) + { + // Arrange + var stream = new MemoryStream([1, 2, 3]); + var remoteFilePath = "/foo/bar/ping.bin"; + + var mockedNativeFileUploaderProxy = new MockedGreenNativeFileUploaderProxySpy140(uploaderCallbacksProxy: new GenericNativeFileUploaderCallbacksProxy_()); + var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); + + AppleTidbits.KnownProblematicDevices.Add(("iphone 6", "apple")); + + using var eventsMonitor = fileUploader.Monitor(); + + // Act + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + + data: stream, + maxTriesCount: 1, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, + byteAlignment: byteAlignment, + + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment + )); + + // Assert + await work.Should().CompleteWithinAsync(200.Seconds()); + + mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); + mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 + mockedNativeFileUploaderProxy.BeginUploadCalled.Should().BeTrue(); + + mockedNativeFileUploaderProxy.ObservedPipelineDepth.Should().Be(expectedPipelineDepth); + mockedNativeFileUploaderProxy.ObservedByteAlignment.Should().Be(expectedByteAlignment); + + mockedNativeFileUploaderProxy.ObservedWindowCapacity.Should().Be(expectedWindowCapacity); + mockedNativeFileUploaderProxy.ObservedInitialMtuSize.Should().Be(expectedInitialMtuSize); + mockedNativeFileUploaderProxy.ObservedMemoryAlignment.Should().Be(expectedMemoryAlignment); + + eventsMonitor + .OccurredEvents.Where(x => x.EventName == nameof(fileUploader.FatalErrorOccurred)) + .Should().HaveCount(0); + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileUploaderState.Uploading); + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileUploaderState.Complete); + + eventsMonitor + .Should().Raise(nameof(fileUploader.FileUploaded)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath); + + //00 we dont want to disconnect the device regardless of the outcome + } + + private class MockedGreenNativeFileUploaderProxySpy140 : MockedNativeFileUploaderProxySpy + { + public int? ObservedPipelineDepth { get; private set; } + public int? ObservedByteAlignment { get; private set; } + + public int? ObservedInitialMtuSize { get; private set; } + public int? ObservedWindowCapacity { get; private set; } + public int? ObservedMemoryAlignment { get; private set; } + + public MockedGreenNativeFileUploaderProxySpy140(INativeFileUploaderCallbacksProxy uploaderCallbacksProxy) : base(uploaderCallbacksProxy) + { + } + + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) + { + ObservedPipelineDepth = pipelineDepth; + ObservedByteAlignment = byteAlignment; + + ObservedInitialMtuSize = initialMtuSize; + ObservedWindowCapacity = windowCapacity; + ObservedMemoryAlignment = memoryAlignment; + + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); + + Task.Run(async () => //00 vital + { + await Task.Delay(10); + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Idle, EFileUploaderState.Uploading); + + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(00, 00); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(10, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(20, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(30, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(40, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(50, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(60, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(70, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(80, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(90, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(100, 10); + + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Complete); // order + FileUploadedAdvertisement(remoteFilePath); // order + }); + + return verdict; + + //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native uploader + } + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyConnection.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyConnection.cs new file mode 100644 index 00000000..ead65911 --- /dev/null +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyConnection.cs @@ -0,0 +1,206 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Constants; +using Laerdal.McuMgr.Common.Enums; +using Laerdal.McuMgr.Common.Events; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; +using Laerdal.McuMgr.FileUploader.Contracts.Events; +using Laerdal.McuMgr.FileUploader.Contracts.Native; +using GenericNativeFileUploaderCallbacksProxy_ = Laerdal.McuMgr.FileUploader.FileUploader.GenericNativeFileUploaderCallbacksProxy; + +#pragma warning disable xUnit1026 + +namespace Laerdal.McuMgr.Tests.FileUploader +{ + public partial class FileUploaderTestbed + { + [Theory] + [InlineData("FUT.SFUA.SCSBFBTFS.GFBC.010", "/path/to/file.bin", 2)] + [InlineData("FUT.SFUA.SCSBFBTFS.GFBC.020", "/path/to/file.bin", 3)] + [InlineData("FUT.SFUA.SCSBFBTFS.GFBC.030", "/path/to/file.bin", 5)] + public async Task SingleFileUploadAsync_ShouldCompleteSuccessfullyByFallingBackToFailsafeSettings_GivenFlakyBluetoothConnection(string testcaseNickname, string remoteFilePath, int maxTriesCount) + { + // Arrange + var stream = new MemoryStream([1, 2, 3]); + + var mockedNativeFileUploaderProxy = new MockedGreenNativeFileUploaderProxySpy120( + maxTriesCount: maxTriesCount, + uploaderCallbacksProxy: new GenericNativeFileUploaderCallbacksProxy_() + ); + var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); + + using var eventsMonitor = fileUploader.Monitor(); + + // Act + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: stream, + maxTriesCount: maxTriesCount, + remoteFilePath: remoteFilePath + )); + + // Assert + await work.Should().CompleteWithinAsync((maxTriesCount * 2).Seconds()); + + mockedNativeFileUploaderProxy.BugDetected.Should().BeNull(); + mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); + mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 + mockedNativeFileUploaderProxy.BeginUploadCalled.Should().BeTrue(); + + eventsMonitor + .OccurredEvents.Where(x => x.EventName == nameof(fileUploader.FatalErrorOccurred)) + .Should().HaveCount(maxTriesCount - 1); //one error for each try except the last one + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileUploaderState.Uploading); + + eventsMonitor + .OccurredEvents + .Where(x => x.EventName == nameof(fileUploader.LogEmitted)) + .SelectMany(x => x.Parameters) + .OfType() + .Count(l => l is { Level: ELogLevel.Warning } && l.Message.Contains("[FU.UA.010]")) + .Should() + .Be(1); + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileUploaderState.Complete); + + eventsMonitor + .Should().Raise(nameof(fileUploader.FileUploaded)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath); + + //00 we dont want to disconnect the device regardless of the outcome + } + + private class MockedGreenNativeFileUploaderProxySpy120 : MockedNativeFileUploaderProxySpy + { + private readonly int _maxTriesCount; + + public string BugDetected { get; private set; } + + public MockedGreenNativeFileUploaderProxySpy120(INativeFileUploaderCallbacksProxy uploaderCallbacksProxy, int maxTriesCount) : base(uploaderCallbacksProxy) + { + _maxTriesCount = maxTriesCount; + } + + private int _tryCounter; + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) + { + _tryCounter++; + + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); + + Task.Run(async () => //00 vital + { + await Task.Delay(10); + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Idle, EFileUploaderState.Uploading); + + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(00, 00); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(10, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(20, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(30, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(40, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(50, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(60, 10); + + if (_tryCounter == _maxTriesCount && initialMtuSize != AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(initialMtuSize)} set to {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize} but it's set to {initialMtuSize?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, BugDetected, EGlobalErrorCode.Generic); // order + return; + } + + if (_tryCounter == _maxTriesCount && windowCapacity != AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(windowCapacity)} set to {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity} but it's set to {windowCapacity?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, BugDetected, EGlobalErrorCode.Generic); // order + return; + } + + if (_tryCounter == _maxTriesCount && memoryAlignment != AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(memoryAlignment)} set to {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment} but it's set to {memoryAlignment?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, BugDetected, EGlobalErrorCode.Generic); // order + return; + } + + if (_tryCounter == _maxTriesCount && pipelineDepth != AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(pipelineDepth)} set to {AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth} but it's set to {pipelineDepth?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, BugDetected, EGlobalErrorCode.Generic); // order + return; + } + + if (_tryCounter == _maxTriesCount && byteAlignment != AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(byteAlignment)} set to {AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment} but it's set to {byteAlignment?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, BugDetected, EGlobalErrorCode.Generic); // order + return; + } + + if (_tryCounter < _maxTriesCount) + { + await Task.Delay(20); + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EGlobalErrorCode.Generic); // order + return; + } + + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(70, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(80, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(90, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(100, 10); + + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Complete); // order + FileUploadedAdvertisement(remoteFilePath); // order + }); + + return verdict; + + //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native uploader + } + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs index 2813645a..a478b6b8 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs @@ -20,10 +20,15 @@ public partial class FileUploaderTestbed [InlineData("FUT.SFUA.SCS.GGNFD.050", "/path/to/file.bin", 3, -100)] [InlineData("FUT.SFUA.SCS.GGNFD.060", "/path/to/file.bin", 3, +000)] [InlineData("FUT.SFUA.SCS.GGNFD.070", "/path/to/file.bin", 3, +100)] - public async Task SingleFileUploadAsync_ShouldCompleteSuccessfully_GivenGreenNativeFileUploader(string testcaseNickname, string remoteFilePath, int maxTriesCount, int sleepTimeBetweenRetriesInMs) + public async Task SingleFileUploadAsync_ShouldCompleteSuccessfully_GivenGreenNativeFileUploader( + string testcaseNickname, + string remoteFilePath, + int maxTriesCount, + int sleepTimeBetweenRetriesInMs + ) { // Arrange - var expectedRemoteFilepath = remoteFilePath.StartsWith("/") + var expectedRemoteFilepath = remoteFilePath.StartsWith('/') ? remoteFilePath : $"/{remoteFilePath}"; @@ -37,6 +42,9 @@ public async Task SingleFileUploadAsync_ShouldCompleteSuccessfully_GivenGreenNat // Act var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + data: new byte[] { 1, 2, 3 }, maxTriesCount: maxTriesCount, remoteFilePath: remoteFilePath, @@ -82,11 +90,29 @@ public MockedGreenNativeFileUploaderProxySpy(INativeFileUploaderCallbacksProxy u } private int _tryCount; - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { _tryCount++; - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { @@ -97,7 +123,7 @@ public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] d if (_tryCount < _maxNumberOfTriesForSuccess) { StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); - FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EMcuMgrErrorCode.Corrupt, EFileUploaderGroupReturnCode.Unset); + FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EGlobalErrorCode.Generic); return; } diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfully_GivenGreenStream.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfully_GivenGreenStream.cs new file mode 100644 index 00000000..7298ef5e --- /dev/null +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldCompleteSuccessfully_GivenGreenStream.cs @@ -0,0 +1,166 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Constants; +using Laerdal.McuMgr.Common.Enums; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; +using Laerdal.McuMgr.FileUploader.Contracts.Events; +using Laerdal.McuMgr.FileUploader.Contracts.Native; +using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; +using GenericNativeFileUploaderCallbacksProxy_ = Laerdal.McuMgr.FileUploader.FileUploader.GenericNativeFileUploaderCallbacksProxy; + +#pragma warning disable xUnit1026 + +namespace Laerdal.McuMgr.Tests.FileUploader +{ + public partial class FileUploaderTestbed + { + [Theory] + [InlineData("FUT.SFUA.SCS.GVSTBR.010", "path/to/file.bin", 01, +100)] // this should be normalized to /path/to/file.bin + [InlineData("FUT.SFUA.SCS.GVSTBR.020", "/path/to/file.bin", 2, -100)] // negative sleep time should be interpreted as 0 + [InlineData("FUT.SFUA.SCS.GVSTBR.030", "/path/to/file.bin", 2, +000)] + [InlineData("FUT.SFUA.SCS.GVSTBR.040", "/path/to/file.bin", 2, +100)] + [InlineData("FUT.SFUA.SCS.GVSTBR.050", "/path/to/file.bin", 3, -100)] + [InlineData("FUT.SFUA.SCS.GVSTBR.060", "/path/to/file.bin", 3, +000)] + [InlineData("FUT.SFUA.SCS.GVSTBR.070", "/path/to/file.bin", 3, +100)] + public async Task SingleFileUploadAsync_ShouldCompleteSuccessfully_GivenVariousSleepTimesBetweenRetries(string testcaseNickname, string remoteFilePath, int maxTriesCount, int sleepTimeBetweenRetriesInMs) + { + // Arrange + var stream = new MemoryStream([1, 2, 3]); + + var expectedRemoteFilepath = remoteFilePath.StartsWith('/') + ? remoteFilePath + : $"/{remoteFilePath}"; + + var mockedNativeFileUploaderProxy = new MockedGreenNativeFileUploaderProxySpy100( + maxTriesCount: maxTriesCount, + uploaderCallbacksProxy: new GenericNativeFileUploaderCallbacksProxy_() + ); + var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); + + using var eventsMonitor = fileUploader.Monitor(); + + // Act + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: stream, + maxTriesCount: maxTriesCount, + remoteFilePath: remoteFilePath, + sleepTimeBetweenRetriesInMs: sleepTimeBetweenRetriesInMs + )); + + // Assert + await work.Should().CompleteWithinAsync(((maxTriesCount + 1) * 2).Seconds()); + + mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); + mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 + mockedNativeFileUploaderProxy.BeginUploadCalled.Should().BeTrue(); + + eventsMonitor + .OccurredEvents.Where(x => x.EventName == nameof(fileUploader.FatalErrorOccurred)) + .Should().HaveCount(maxTriesCount - 1); //one error for each try except the last one + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == expectedRemoteFilepath && args.NewState == EFileUploaderState.Uploading); + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == expectedRemoteFilepath && args.NewState == EFileUploaderState.Complete); + + eventsMonitor + .Should().Raise(nameof(fileUploader.FileUploaded)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == expectedRemoteFilepath); + + //00 we dont want to disconnect the device regardless of the outcome + } + + private class MockedGreenNativeFileUploaderProxySpy100 : MockedNativeFileUploaderProxySpy + { + private readonly int _maxTriesCount; + + public MockedGreenNativeFileUploaderProxySpy100(INativeFileUploaderCallbacksProxy uploaderCallbacksProxy, int maxTriesCount) : base(uploaderCallbacksProxy) + { + _maxTriesCount = maxTriesCount; + } + + private int _tryCounter; + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) + { + _tryCounter++; + + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); + + Task.Run(async () => //00 vital + { + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Idle, EFileUploaderState.Idle); + await Task.Delay(10); + + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Idle, EFileUploaderState.Uploading); + await Task.Delay(10); + + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(00, 00); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(10, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(20, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(30, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(40, 10); + await Task.Delay(5); + + if (_tryCounter < _maxTriesCount) + { + await Task.Delay(20); + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EGlobalErrorCode.Generic); // order + return; + } + + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(50, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(60, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(70, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(80, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(90, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(100, 10); + + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Complete); // order + FileUploadedAdvertisement(remoteFilePath); // order + }); + + return verdict; + + //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native uploader + } + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldHaveStreamAutodisposed_GivenBooleanAutodisposedParameter.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldHaveStreamAutodisposed_GivenBooleanAutodisposedParameter.cs new file mode 100644 index 00000000..d56b4b63 --- /dev/null +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldHaveStreamAutodisposed_GivenBooleanAutodisposedParameter.cs @@ -0,0 +1,154 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Enums; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; +using Laerdal.McuMgr.FileUploader.Contracts.Events; +using Laerdal.McuMgr.FileUploader.Contracts.Native; +using GenericNativeFileUploaderCallbacksProxy_ = Laerdal.McuMgr.FileUploader.FileUploader.GenericNativeFileUploaderCallbacksProxy; + +#pragma warning disable xUnit1026 + +namespace Laerdal.McuMgr.Tests.FileUploader +{ + public partial class FileUploaderTestbed + { + [Theory] + [InlineData("FUT.SFUA.SCAGS.GBAP.010", "stream", true)] + [InlineData("FUT.SFUA.SCAGS.GBAP.020", "stream", false)] + [InlineData("FUT.SFUA.SCAGS.GBAP.030", "func_stream", true)] + [InlineData("FUT.SFUA.SCAGS.GBAP.040", "func_stream", false)] + [InlineData("FUT.SFUA.SCAGS.GBAP.050", "func_task_stream", true)] + [InlineData("FUT.SFUA.SCAGS.GBAP.060", "func_task_stream", false)] + [InlineData("FUT.SFUA.SCAGS.GBAP.070", "func_valuetask_stream", true)] + [InlineData("FUT.SFUA.SCAGS.GBAP.080", "func_valuetask_stream", false)] + public async Task SingleFileUploadAsync_ShouldConditionallyAutodisposeGivenStream_GivenBooleanAutodisposedParameter(string testcaseNickname, string streamType, bool shouldAutodisposeStream) + { + // Arrange + var stream = new MemoryStream([1, 2, 3]); + var streamProvider = streamType switch + { + "stream" => (object)stream, + "func_stream" => () => stream, + "func_task_stream" => () => Task.FromResult(stream), + "func_valuetask_stream" => () => new ValueTask(stream), + _ => throw new NotImplementedException($"Wops! Don't know how to handle stream type {streamType}! (how did this happen?)") + }; + + const string remoteFilePath = "/foo/bar/ping.bin"; + + var mockedNativeFileUploaderProxy = new MockedGreenNativeFileUploaderProxySpy110(new GenericNativeFileUploaderCallbacksProxy_()); + var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); + + using var eventsMonitor = fileUploader.Monitor(); + + // Act + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: streamProvider, + maxTriesCount: 1, + remoteFilePath: remoteFilePath, + autodisposeStream: shouldAutodisposeStream + )); + + // Assert + await work.Should().CompleteWithinAsync(2.Seconds()); + + mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); + mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 + mockedNativeFileUploaderProxy.BeginUploadCalled.Should().BeTrue(); + + eventsMonitor + .OccurredEvents.Where(x => x.EventName == nameof(fileUploader.FatalErrorOccurred)) + .Should().HaveCount(0); + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileUploaderState.Uploading); + + eventsMonitor + .Should().Raise(nameof(fileUploader.StateChanged)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath && args.NewState == EFileUploaderState.Complete); + + eventsMonitor + .Should().Raise(nameof(fileUploader.FileUploaded)) + .WithSender(fileUploader) + .WithArgs(args => args.Resource == remoteFilePath); + + stream.CanRead.Should().Be(!shouldAutodisposeStream); //10 + + //00 we dont want to disconnect the device regardless of the outcome + //10 check if the stream was disposed or not based on the value of the autodisposeStream parameter + } + + private class MockedGreenNativeFileUploaderProxySpy110 : MockedNativeFileUploaderProxySpy + { + public MockedGreenNativeFileUploaderProxySpy110(INativeFileUploaderCallbacksProxy uploaderCallbacksProxy) : base(uploaderCallbacksProxy) + { + } + + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) + { + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); + + Task.Run(async () => //00 vital + { + await Task.Delay(10); + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Idle, EFileUploaderState.Uploading); + + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(00, 00); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(10, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(20, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(30, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(40, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(50, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(60, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(70, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(80, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(90, 10); + await Task.Delay(5); + FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(100, 10); + + await Task.Delay(20); + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Complete); // order + FileUploadedAdvertisement(remoteFilePath); // order + }); + + return verdict; + + //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native uploader + } + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs index 4857cbfa..eadf067f 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs @@ -30,6 +30,9 @@ public async Task SingleFileUploadAsync_ShouldThrowAllUploadAttemptsFailedExcept // Act var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + data: mockedFileData, maxTriesCount: maxTriesCount, remoteFilePath: remoteFilePath @@ -72,9 +75,27 @@ public MockedGreenNativeFileUploaderProxySpy4(INativeFileUploaderCallbacksProxy { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { @@ -84,8 +105,8 @@ public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] d await Task.Delay(2_000); - StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order - FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EMcuMgrErrorCode.Corrupt, EFileUploaderGroupReturnCode.Unset); // order + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EGlobalErrorCode.Generic); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs index 07918f0a..1938baf5 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs @@ -25,7 +25,7 @@ public async Task SingleFileUploadAsync_ShouldThrowAllUploadAttemptsFailedExcept var mockedNativeFileUploaderProxy = new MockedErroneousNativeFileUploaderProxySpy13( uploaderCallbacksProxy: new GenericNativeFileUploaderCallbacksProxy_(), - nativeErrorMessageForFileNotFound: nativeRogueErrorMessage + nativeRogueErrorMessage: nativeRogueErrorMessage ); var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); @@ -33,6 +33,9 @@ public async Task SingleFileUploadAsync_ShouldThrowAllUploadAttemptsFailedExcept // Act var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + data: mockedFileData, //doesnt really matter we just want to ensure that the method fails early and doesnt retry maxTriesCount: maxTriesCount, remoteFilePath: remoteFilePath, @@ -75,16 +78,34 @@ await work.Should() private class MockedErroneousNativeFileUploaderProxySpy13 : MockedNativeFileUploaderProxySpy { - private readonly string _nativeErrorMessageForFileNotFound; + private readonly string _nativeRogueErrorMessage; - public MockedErroneousNativeFileUploaderProxySpy13(INativeFileUploaderCallbacksProxy uploaderCallbacksProxy, string nativeErrorMessageForFileNotFound) : base(uploaderCallbacksProxy) + public MockedErroneousNativeFileUploaderProxySpy13(INativeFileUploaderCallbacksProxy uploaderCallbacksProxy, string nativeRogueErrorMessage) : base(uploaderCallbacksProxy) { - _nativeErrorMessageForFileNotFound = nativeErrorMessageForFileNotFound; + _nativeRogueErrorMessage = nativeRogueErrorMessage; } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { @@ -94,8 +115,8 @@ public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] d await Task.Delay(100); - StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order - FatalErrorOccurredAdvertisement(remoteFilePath, _nativeErrorMessageForFileNotFound, EMcuMgrErrorCode.Corrupt, EFileUploaderGroupReturnCode.Unset); // order + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, _nativeRogueErrorMessage, EGlobalErrorCode.Generic); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs index 94dd02c2..2111487b 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs @@ -21,7 +21,13 @@ public async Task SingleFileUploadAsync_ShouldThrowArgumentException_GivenEmptyR using var eventsMonitor = fileUploader.Monitor(); // Act - var work = new Func(() => fileUploader.UploadAsync(mockedFileData, remoteFilePath)); + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: mockedFileData, + remoteFilePath: remoteFilePath + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); @@ -42,9 +48,27 @@ public MockedGreenNativeFileUploaderProxySpy2(INativeFileUploaderCallbacksProxy { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs index 8ec98478..40dd16f0 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs @@ -15,12 +15,12 @@ namespace Laerdal.McuMgr.Tests.FileUploader public partial class FileUploaderTestbed { [Theory] - [InlineData("FDT.SFUA.STRFNFE.GNEF.010", EMcuMgrErrorCode.Unknown, "UNKNOWN (1)", 1)] //android + ios - [InlineData("FDT.SFUA.STRFNFE.GNEF.020", EMcuMgrErrorCode.Unknown, "UNKNOWN (1)", 2)] //android + ios - [InlineData("FDT.SFUA.STRFNFE.GNEF.030", EMcuMgrErrorCode.Unknown, "UNKNOWN (1)", 3)] //android + ios + [InlineData("FDT.SFUA.STRFNFE.GNEF.010", EGlobalErrorCode.SubSystemFilesystem_NotFound, "UNKNOWN (1)", 1)] //android + ios + [InlineData("FDT.SFUA.STRFNFE.GNEF.020", EGlobalErrorCode.SubSystemFilesystem_NotFound, "UNKNOWN (1)", 2)] //android + ios + [InlineData("FDT.SFUA.STRFNFE.GNEF.030", EGlobalErrorCode.SubSystemFilesystem_NotFound, "UNKNOWN (1)", 3)] //android + ios public async Task SingleFileUploadAsync_ShouldThrowRemoteFolderNotFoundException_GivenNonExistFolderInPath( string testcaseNickname, - EMcuMgrErrorCode mcuMgrErrorCode, + EGlobalErrorCode globalErrorCode, string nativeErrorMessageForFileNotFound, int maxTriesCount ) @@ -32,7 +32,7 @@ int maxTriesCount var mockedNativeFileUploaderProxy = new MockedErroneousNativeFileUploaderProxySpy2( mockedFileData: mockedFileData, uploaderCallbacksProxy: new GenericNativeFileUploaderCallbacksProxy_(), - mcuMgrErrorCode: mcuMgrErrorCode, + globalErrorCode: globalErrorCode, nativeErrorMessageForFileNotFound: nativeErrorMessageForFileNotFound ); var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); @@ -41,6 +41,9 @@ int maxTriesCount // Act var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + data: mockedFileData, //doesnt really matter we just want to ensure that the method fails early and doesnt retry maxTriesCount: maxTriesCount, remoteFilePath: remoteFilePath, @@ -84,24 +87,42 @@ await work.Should() private class MockedErroneousNativeFileUploaderProxySpy2 : MockedNativeFileUploaderProxySpy { - private readonly EMcuMgrErrorCode _mcuMgrErrorCode; + private readonly EGlobalErrorCode _globalErrorCode; private readonly string _nativeErrorMessageForFileNotFound; public MockedErroneousNativeFileUploaderProxySpy2( INativeFileUploaderCallbacksProxy uploaderCallbacksProxy, byte[] mockedFileData, - EMcuMgrErrorCode mcuMgrErrorCode, + EGlobalErrorCode globalErrorCode, string nativeErrorMessageForFileNotFound ) : base(uploaderCallbacksProxy) { _ = mockedFileData; - _mcuMgrErrorCode = mcuMgrErrorCode; + _globalErrorCode = globalErrorCode; _nativeErrorMessageForFileNotFound = nativeErrorMessageForFileNotFound; } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { @@ -111,8 +132,8 @@ public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] d await Task.Delay(100); - StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order - FatalErrorOccurredAdvertisement(remoteFilePath, _nativeErrorMessageForFileNotFound, _mcuMgrErrorCode, EFileUploaderGroupReturnCode.Unset); // order + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, _nativeErrorMessageForFileNotFound, _globalErrorCode); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUnauthorizedErrorException_GivenAccessDeniedNativeErrorMessage.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUnauthorizedErrorException_GivenAccessDeniedNativeErrorMessage.cs index bde5a23c..6749f619 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUnauthorizedErrorException_GivenAccessDeniedNativeErrorMessage.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUnauthorizedErrorException_GivenAccessDeniedNativeErrorMessage.cs @@ -17,10 +17,20 @@ public async Task SingleFileUploadAsync_ShouldThrowUnauthorizedErrorException_Gi var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); // Act - var work = new Func(() => fileUploader.UploadAsync(data: new byte[] { 1 }, remoteFilePath: "/path/to/file.bin", maxTriesCount: 2)); + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: new byte[] { 1 }, + maxTriesCount: 2, + remoteFilePath: "/path/to/file.bin" + )); // Assert - (await work.Should().ThrowExactlyAsync()).WithInnerExceptionExactly(); + (await work.Should().ThrowExactlyAsync()) + .WithInnerExceptionExactly() + .And + .GlobalErrorCode.Should().Be(EGlobalErrorCode.McuMgrErrorBeforeSmpV2_AccessDenied); mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 @@ -36,9 +46,27 @@ public MockedErroneousNativeFileUploaderProxySpy100(INativeFileUploaderCallbacks { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { @@ -48,8 +76,8 @@ public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] d await Task.Delay(100); - StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order - FatalErrorOccurredAdvertisement(remoteFilePath, "blah blah", EMcuMgrErrorCode.AccessDenied, EFileUploaderGroupReturnCode.Unset); // order + StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order + FatalErrorOccurredAdvertisement(remoteFilePath, "blah blah", EGlobalErrorCode.McuMgrErrorBeforeSmpV2_AccessDenied); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs index 6391e52e..a05b76f0 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs @@ -20,6 +20,7 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadCancelledException_Give // Arrange var mockedFileData = new byte[] { 1, 2, 3 }; const string remoteFilePath = "/path/to/file.bin"; + const string cancellationReason = "blah blah foobar"; var mockedNativeFileUploaderProxy = new MockedGreenNativeFileUploaderProxySpy3(new GenericNativeFileUploaderCallbacksProxy_(), isCancellationLeadingToSoftLanding); var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); @@ -31,14 +32,22 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadCancelledException_Give { await Task.Delay(500); - fileUploader.Cancel(); + fileUploader.Cancel(reason: cancellationReason); }); - var work = new Func(() => fileUploader.UploadAsync(mockedFileData, remoteFilePath)); + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: mockedFileData, + remoteFilePath: remoteFilePath + )); // Assert await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); mockedNativeFileUploaderProxy.CancelCalled.Should().BeTrue(); + mockedNativeFileUploaderProxy.CancellationReason.Should().Be(cancellationReason); + mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 mockedNativeFileUploaderProxy.BeginUploadCalled.Should().BeTrue(); @@ -67,19 +76,20 @@ public MockedGreenNativeFileUploaderProxySpy3(INativeFileUploaderCallbacksProxy _isCancellationLeadingToSoftLanding = isCancellationLeadingToSoftLanding; } - public override void Cancel() + public override void Cancel(string reason = "") { - base.Cancel(); + base.Cancel(reason); Task.Run(async () => // under normal circumstances the native implementation will bubble up these events in this exact order { + CancellingAdvertisement(reason); // order StateChangedAdvertisement(_currentRemoteFilePath, oldState: EFileUploaderState.Idle, newState: EFileUploaderState.Cancelling); // order await Task.Delay(100); if (_isCancellationLeadingToSoftLanding) //00 { StateChangedAdvertisement(_currentRemoteFilePath, oldState: EFileUploaderState.Idle, newState: EFileUploaderState.Cancelled); // order - CancelledAdvertisement(); // order + CancelledAdvertisement(reason); // order } }); @@ -88,7 +98,15 @@ public override void Cancel() // a best effort basis and this is exactly what we are testing here } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { (FileUploader as IFileUploaderEventSubscribable)!.Cancelled += (sender, args) => { @@ -98,7 +116,17 @@ public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] d _currentRemoteFilePath = remoteFilePath; _cancellationTokenSource = new CancellationTokenSource(); - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadInternalErrorException_GivenErroneousNativeFileUploader.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadInternalErrorException_GivenErroneousNativeFileUploader.cs index 02dc69d8..4d0670ba 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadInternalErrorException_GivenErroneousNativeFileUploader.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadInternalErrorException_GivenErroneousNativeFileUploader.cs @@ -16,7 +16,13 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadInternalErrorException_ var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); // Act - var work = new Func(() => fileUploader.UploadAsync(data: new byte[] { 1 }, remoteFilePath: "/path/to/file.bin")); + var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: new byte[] { 1 }, + remoteFilePath: "/path/to/file.bin" + )); // Assert (await work.Should().ThrowExactlyAsync()).WithInnerExceptionExactly("native symbols not loaded blah blah"); @@ -34,9 +40,27 @@ public MockedErroneousNativeFileUploaderProxySpy(INativeFileUploaderCallbacksPro { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - base.BeginUpload(remoteFilePath, data); + base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Thread.Sleep(100); diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs index 2cf1044a..4e0e1ca4 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs @@ -24,6 +24,9 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadTimeoutException_GivenT // Act var work = new Func(() => fileUploader.UploadAsync( + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + data: new byte[] { 1 }, remoteFilePath: remoteFilePath, timeoutForUploadInMs: 100 @@ -55,9 +58,27 @@ public MockedGreenButSlowNativeFileUploaderProxySpy(INativeFileUploaderCallbacks { } - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public override EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { - var verdict = base.BeginUpload(remoteFilePath, data); + var verdict = base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only + ); Task.Run(async () => //00 vital { diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleStreamUploadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleStreamUploadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs deleted file mode 100644 index 890039ed..00000000 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleStreamUploadAsync.ShouldCompleteSuccessfully_GivenGreenNativeFileDownloader.cs +++ /dev/null @@ -1,116 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Extensions; -using Laerdal.McuMgr.Common.Enums; -using Laerdal.McuMgr.FileUploader.Contracts.Enums; -using Laerdal.McuMgr.FileUploader.Contracts.Events; -using Laerdal.McuMgr.FileUploader.Contracts.Native; -using GenericNativeFileUploaderCallbacksProxy_ = Laerdal.McuMgr.FileUploader.FileUploader.GenericNativeFileUploaderCallbacksProxy; - -#pragma warning disable xUnit1026 - -namespace Laerdal.McuMgr.Tests.FileUploader -{ - public partial class FileUploaderTestbed - { - [Theory] - [InlineData("FUT.SSUA.SCS.GGNFD.010", "path/to/file.bin", 01, +100)] // this should be normalized to /path/to/file.bin - [InlineData("FUT.SSUA.SCS.GGNFD.020", "/path/to/file.bin", 2, -100)] // negative sleep time should be interpreted as 0 - [InlineData("FUT.SSUA.SCS.GGNFD.030", "/path/to/file.bin", 2, +000)] - [InlineData("FUT.SSUA.SCS.GGNFD.040", "/path/to/file.bin", 2, +100)] - [InlineData("FUT.SSUA.SCS.GGNFD.050", "/path/to/file.bin", 3, -100)] - [InlineData("FUT.SSUA.SCS.GGNFD.060", "/path/to/file.bin", 3, +000)] - [InlineData("FUT.SSUA.SCS.GGNFD.070", "/path/to/file.bin", 3, +100)] - public async Task SingleStreamUploadAsync_ShouldCompleteSuccessfully_GivenGreenNativeFileUploader(string testcaseNickname, string remoteFilePath, int maxTriesCount, int sleepTimeBetweenRetriesInMs) - { - // Arrange - var stream = new MemoryStream(new byte[] { 1, 2, 3 }); - - var expectedRemoteFilepath = remoteFilePath.StartsWith("/") - ? remoteFilePath - : $"/{remoteFilePath}"; - - var mockedNativeFileUploaderProxy = new MockedGreenNativeFileUploaderProxySpy100( - uploaderCallbacksProxy: new GenericNativeFileUploaderCallbacksProxy_(), - maxNumberOfTriesForSuccess: maxTriesCount - ); - var fileUploader = new McuMgr.FileUploader.FileUploader(mockedNativeFileUploaderProxy); - - using var eventsMonitor = fileUploader.Monitor(); - - // Act - var work = new Func(() => fileUploader.UploadAsync( - data: stream, - maxTriesCount: maxTriesCount, - remoteFilePath: remoteFilePath, - sleepTimeBetweenRetriesInMs: sleepTimeBetweenRetriesInMs - )); - - // Assert - await work.Should().CompleteWithinAsync(((maxTriesCount + 1) * 2).Seconds()); - - mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); - mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 - mockedNativeFileUploaderProxy.BeginUploadCalled.Should().BeTrue(); - - eventsMonitor - .OccurredEvents.Where(x => x.EventName == nameof(fileUploader.FatalErrorOccurred)) - .Should().HaveCount(maxTriesCount - 1); //one error for each try except the last one - - eventsMonitor - .Should().Raise(nameof(fileUploader.StateChanged)) - .WithSender(fileUploader) - .WithArgs(args => args.Resource == expectedRemoteFilepath && args.NewState == EFileUploaderState.Uploading); - - eventsMonitor - .Should().Raise(nameof(fileUploader.StateChanged)) - .WithSender(fileUploader) - .WithArgs(args => args.Resource == expectedRemoteFilepath && args.NewState == EFileUploaderState.Complete); - - eventsMonitor - .Should().Raise(nameof(fileUploader.FileUploaded)) - .WithSender(fileUploader) - .WithArgs(args => args.Resource == expectedRemoteFilepath); - - //00 we dont want to disconnect the device regardless of the outcome - } - - private class MockedGreenNativeFileUploaderProxySpy100 : MockedNativeFileUploaderProxySpy - { - private readonly int _maxNumberOfTriesForSuccess; - - public MockedGreenNativeFileUploaderProxySpy100(INativeFileUploaderCallbacksProxy uploaderCallbacksProxy, int maxNumberOfTriesForSuccess) : base(uploaderCallbacksProxy) - { - _maxNumberOfTriesForSuccess = maxNumberOfTriesForSuccess; - } - - private int _tryCount; - public override EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) - { - _tryCount++; - - var verdict = base.BeginUpload(remoteFilePath, data); - - Task.Run(async () => //00 vital - { - await Task.Delay(10); - StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Idle, EFileUploaderState.Uploading); - - await Task.Delay(20); - if (_tryCount < _maxNumberOfTriesForSuccess) - { - StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Error); // order - FatalErrorOccurredAdvertisement(remoteFilePath, "fatal error occurred", EMcuMgrErrorCode.Corrupt, EFileUploaderGroupReturnCode.Unset); // order - return; - } - - StateChangedAdvertisement(remoteFilePath, EFileUploaderState.Uploading, EFileUploaderState.Complete); // order - FileUploadedAdvertisement(remoteFilePath); // order - }); - - return verdict; - - //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native uploader - } - } - } -} \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.cs index 5dbc7fcd..8a4d4e22 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.cs @@ -14,6 +14,7 @@ public partial class FileUploaderTestbed public bool CancelCalled { get; private set; } public bool DisconnectCalled { get; private set; } public bool BeginUploadCalled { get; private set; } + public string CancellationReason { get; private set; } public string LastFatalErrorMessage => ""; @@ -28,16 +29,25 @@ protected MockedNativeFileUploaderProxySpy(INativeFileUploaderCallbacksProxy upl _uploaderCallbacksProxy = uploaderCallbacksProxy; } - public virtual EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public virtual EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null + ) { BeginUploadCalled = true; return EFileUploaderVerdict.Success; } - public virtual void Cancel() + public virtual void Cancel(string reason = "") { CancelCalled = true; + CancellationReason = reason; } public virtual void Disconnect() @@ -45,8 +55,11 @@ public virtual void Disconnect() DisconnectCalled = true; } - public void CancelledAdvertisement() - => _uploaderCallbacksProxy.CancelledAdvertisement(); //raises the actual event + public void CancellingAdvertisement(string reason = "") + => _uploaderCallbacksProxy.CancellingAdvertisement(reason); //raises the actual event + + public void CancelledAdvertisement(string reason = "") + => _uploaderCallbacksProxy.CancelledAdvertisement(reason); //raises the actual event public void LogMessageAdvertisement(string message, string category, ELogLevel level, string resource) => _uploaderCallbacksProxy.LogMessageAdvertisement(message, category, level, resource); //raises the actual event @@ -63,9 +76,8 @@ public void FileUploadedAdvertisement(string resource) public void FatalErrorOccurredAdvertisement( string resource, string errorMessage, - EMcuMgrErrorCode errorCode, - EFileUploaderGroupReturnCode fileUploaderGroupReturnCode - ) => _uploaderCallbacksProxy.FatalErrorOccurredAdvertisement(resource, errorMessage, errorCode, fileUploaderGroupReturnCode); //raises the actual event + EGlobalErrorCode globalErrorCode + ) => _uploaderCallbacksProxy.FatalErrorOccurredAdvertisement(resource, errorMessage, globalErrorCode); //raises the actual event public void FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput) => _uploaderCallbacksProxy.FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage, averageThroughput); //raises the actual event diff --git a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareEraser.cs b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareEraser.cs index 246315f1..9af1a987 100644 --- a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareEraser.cs +++ b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareEraser.cs @@ -32,7 +32,7 @@ public MockedGreenNativeFirmwareEraserProxySpy1(INativeFirmwareEraserCallbacksPr { } - public override void BeginErasure(int imageIndex) + public override EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex) { base.BeginErasure(imageIndex); @@ -44,6 +44,8 @@ public override void BeginErasure(int imageIndex) await Task.Delay(20); StateChangedAdvertisement(EFirmwareErasureState.Erasing, EFirmwareErasureState.Complete); }); + + return EFirmwareErasureInitializationVerdict.Success; //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native eraser } diff --git a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowFirmwareErasureInternalErrorException_GivenErroneousNativeFirmwareEraser.cs b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowFirmwareErasureInternalErrorException_GivenErroneousNativeFirmwareEraser.cs index 54bf08e5..e1bc0dbd 100644 --- a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowFirmwareErasureInternalErrorException_GivenErroneousNativeFirmwareEraser.cs +++ b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowFirmwareErasureInternalErrorException_GivenErroneousNativeFirmwareEraser.cs @@ -40,7 +40,7 @@ public MockedErroneousNativeFirmwareEraserProxySpy(INativeFirmwareEraserCallback { } - public override void BeginErasure(int imageIndex) + public override EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex) { base.BeginErasure(imageIndex); diff --git a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs index 68eb5d48..67277ff5 100644 --- a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs @@ -46,7 +46,7 @@ public MockedGreenButSlowNativeFirmwareEraserProxySpy(INativeFirmwareEraserCallb { } - public override void BeginErasure(int imageIndex) + public override EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex) { base.BeginErasure(imageIndex); @@ -58,6 +58,8 @@ public override void BeginErasure(int imageIndex) await Task.Delay(1_000); StateChangedAdvertisement(oldState: EFirmwareErasureState.Erasing, newState: EFirmwareErasureState.Complete); }); + + return EFirmwareErasureInitializationVerdict.Success; //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native resetter } diff --git a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.cs b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.cs index 9010f196..9e20bece 100644 --- a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.cs +++ b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.cs @@ -27,9 +27,11 @@ protected MockedNativeFirmwareEraserProxySpy(INativeFirmwareEraserCallbacksProxy _eraserCallbacksProxy = eraserCallbacksProxy; } - public virtual void BeginErasure(int imageIndex) + public virtual EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex) { BeginErasureCalled = true; + + return EFirmwareErasureInitializationVerdict.Success; } public virtual void Disconnect() @@ -38,16 +40,16 @@ public virtual void Disconnect() } public void LogMessageAdvertisement(string message, string category, ELogLevel level) - => _eraserCallbacksProxy.LogMessageAdvertisement(message, category, level); //raises the actual event + => _eraserCallbacksProxy?.LogMessageAdvertisement(message, category, level); //raises the actual event public void StateChangedAdvertisement(EFirmwareErasureState oldState, EFirmwareErasureState newState) - => _eraserCallbacksProxy.StateChangedAdvertisement(newState: newState, oldState: oldState); //raises the actual event + => _eraserCallbacksProxy?.StateChangedAdvertisement(newState: newState, oldState: oldState); //raises the actual event public void BusyStateChangedAdvertisement(bool busyNotIdle) - => _eraserCallbacksProxy.BusyStateChangedAdvertisement(busyNotIdle); //raises the actual event + => _eraserCallbacksProxy?.BusyStateChangedAdvertisement(busyNotIdle); //raises the actual event - public void FatalErrorOccurredAdvertisement(string errorMessage) - => _eraserCallbacksProxy.FatalErrorOccurredAdvertisement(errorMessage); //raises the actual event + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) + => _eraserCallbacksProxy?.FatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); //raises the actual event } } } \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.BeginInstallation.ShouldThrowArgumentException_GivenInvalidFirmwareDataBytes.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.BeginInstallation.ShouldThrowArgumentException_GivenInvalidFirmwareDataBytes.cs index 0787c6d0..4104f36d 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.BeginInstallation.ShouldThrowArgumentException_GivenInvalidFirmwareDataBytes.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.BeginInstallation.ShouldThrowArgumentException_GivenInvalidFirmwareDataBytes.cs @@ -21,7 +21,11 @@ public void BeginInstallation_ShouldThrowArgumentException_GivenInvalidFirmwareD using var eventsMonitor = firmwareInstaller.Monitor(); // Act - var work = new Func(() => firmwareInstaller.BeginInstallation(mockedFileData)); + var work = new Func(() => firmwareInstaller.BeginInstallation( + data: mockedFileData, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); // Assert work.Should().Throw(); @@ -46,6 +50,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -58,6 +63,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -65,6 +71,9 @@ public override EFirmwareInstallationVerdict BeginInstallation( Task.Run(async () => //00 vital { + await Task.Delay(10); + StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Idle); + await Task.Delay(10); StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Uploading); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldCompleteSuccessfullyWithLastDitchAttemptUsingLowerInitialMtu_GivenFlakyConnectionForFileUploading.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldCompleteSuccessfullyWithLastDitchAttemptUsingLowerInitialMtu_GivenFlakyConnectionForFileUploading.cs new file mode 100644 index 00000000..a4d93b37 --- /dev/null +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldCompleteSuccessfullyWithLastDitchAttemptUsingLowerInitialMtu_GivenFlakyConnectionForFileUploading.cs @@ -0,0 +1,262 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Laerdal.McuMgr.Common.Constants; +using Laerdal.McuMgr.Common.Enums; +using Laerdal.McuMgr.Common.Events; +using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; +using Laerdal.McuMgr.FirmwareInstaller.Contracts.Events; +using Laerdal.McuMgr.FirmwareInstaller.Contracts.Native; +using GenericNativeFirmwareInstallerCallbacksProxy_ = Laerdal.McuMgr.FirmwareInstaller.FirmwareInstaller.GenericNativeFirmwareInstallerCallbacksProxy; + +#pragma warning disable xUnit1026 + +namespace Laerdal.McuMgr.Tests.FirmwareInstaller +{ + /// + /// Certain exotic devices (like Samsung A8 Android tablets) have buggy ble-stacks and have reliable support only for Phy1M mode and even then they have a + /// problem with establishing a ble-connection with nRF51+ chipsets of Nordic unless BeginInstallation() / UploadAsync() / DownloadAsync() are called + /// with initialMtuValue=23 (which is the only value that works for these exotic devices). + /// + /// We need to ensure that the retry logic is able to handle such problematic devices by lowering the initialMtuValue to 23 in the last few retries + /// (as a last ditch best-effort.) + /// + public partial class FirmwareInstallerTestbed + { + [Theory] + [InlineData("FIT.IA.SCSWLDAULIM.GPDFFU.010", 2)] + [InlineData("FIT.IA.SCSWLDAULIM.GPDFFU.020", 3)] + [InlineData("FIT.IA.SCSWLDAULIM.GPDFFU.030", 5)] + public async Task InstallAsync_ShouldCompleteSuccessfullyWithLastDitchAttemptUsingLowerInitialMtu_GivenFlakyConnectionForFileUploading(string testNickname, int maxTriesCount) + { + // Arrange + var mockedNativeFirmwareInstallerProxy = new MockedGreenNativeFirmwareInstallerProxySpy10(new GenericNativeFirmwareInstallerCallbacksProxy_(), maxTriesCount); + var firmwareInstaller = new McuMgr.FirmwareInstaller.FirmwareInstaller(mockedNativeFirmwareInstallerProxy); + + using var eventsMonitor = firmwareInstaller.Monitor(); + + // Act + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1, 2, 3], + maxTriesCount: maxTriesCount, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); + + // Assert + await work.Should().CompleteWithinAsync(4.Seconds()); + + mockedNativeFirmwareInstallerProxy.BugDetected.Should().BeNull(); + + mockedNativeFirmwareInstallerProxy.CancelCalled.Should().BeFalse(); + mockedNativeFirmwareInstallerProxy.DisconnectCalled.Should().BeFalse(); //00 + mockedNativeFirmwareInstallerProxy.BeginInstallationCalled.Should().BeTrue(); + + eventsMonitor + .OccurredEvents + .Count(x => x.EventName == nameof(firmwareInstaller.FatalErrorOccurred)) + .Should() + .Be(maxTriesCount - 1); + + eventsMonitor + .OccurredEvents //there should be only one completed event + .Count(x => x.Parameters.OfType().FirstOrDefault() is { NewState: EFirmwareInstallationState.Complete }) + .Should() + .Be(1); + + eventsMonitor + .OccurredEvents + .Where(x => x.EventName == nameof(firmwareInstaller.LogEmitted)) + .SelectMany(x => x.Parameters) + .OfType() + .Count(l => l is { Level: ELogLevel.Warning } && l.Message.Contains("[FI.IA.010]")) + .Should() + .Be(1); + + eventsMonitor + .Should().Raise(nameof(firmwareInstaller.StateChanged)) + .WithSender(firmwareInstaller) + .WithArgs(args => args.NewState == EFirmwareInstallationState.Uploading); + + eventsMonitor + .Should() + .Raise(nameof(firmwareInstaller.FirmwareUploadProgressPercentageAndDataThroughputChanged)) + .WithSender(firmwareInstaller); + + //00 we dont want to disconnect the device regardless of the outcome + } + + private class MockedGreenNativeFirmwareInstallerProxySpy10 : MockedNativeFirmwareInstallerProxySpy + { + private int _tryCounter; + private readonly int _maxTriesCount; + + public string BugDetected { get; private set; } + + public MockedGreenNativeFirmwareInstallerProxySpy10(INativeFirmwareInstallerCallbacksProxy firmwareInstallerCallbacksProxy, int maxTriesCount) + : base(firmwareInstallerCallbacksProxy) + { + _maxTriesCount = maxTriesCount; + } + + public override EFirmwareInstallationVerdict BeginInstallation( + byte[] data, + EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, + bool? eraseSettings = null, + int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null, + int? pipelineDepth = null, + int? byteAlignment = null + ) + { + if (BugDetected is not null) + throw new Exception(BugDetected); + + _tryCounter++; + + var verdict = base.BeginInstallation( + data: data, + mode: mode, + eraseSettings: eraseSettings, + pipelineDepth: pipelineDepth, + byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment, + estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds + ); + + Task.Run(function: async () => //00 vital + { + if (_tryCounter == 1 && initialMtuSize == AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize) + { + BugDetected = $"[BUG DETECTED] The very first try should not be with {nameof(initialMtuSize)} set to the fail-safe value of {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize} right off the bat - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == 1 && windowCapacity == AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity) + { + BugDetected = $"[BUG DETECTED] The very first try should not be with {nameof(windowCapacity)} set to the fail-safe value of {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity} right off the bat - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == 1 && memoryAlignment == AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment) + { + BugDetected = $"[BUG DETECTED] The very first try should not be with {nameof(memoryAlignment)} set to the fail-safe value of {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment} right off the bat - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == 1 && pipelineDepth == AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth) + { + BugDetected = $"[BUG DETECTED] The very first try should not be with {nameof(pipelineDepth)} set to the fail-safe value of {AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth} right off the bat - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == 1 && byteAlignment == AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment) + { + BugDetected = $"[BUG DETECTED] The very first try should not be with {nameof(byteAlignment)} set to the fail-safe value of {AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment} right off the bat - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Idle); + await Task.Delay(10); + + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Validating); + await Task.Delay(10); + + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Validating, newState: EFirmwareInstallationState.Uploading); + await Task.Delay(100); + + { //file uploading simulation + FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage: 00, averageThroughput: 00); + await Task.Delay(10); + + if (_tryCounter == _maxTriesCount && initialMtuSize != AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(initialMtuSize)} set to {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize} but it's set to {initialMtuSize?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == _maxTriesCount && windowCapacity != AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(windowCapacity)} set to {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity} but it's set to {windowCapacity?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == _maxTriesCount && memoryAlignment != AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(memoryAlignment)} set to {AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment} but it's set to {memoryAlignment?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == _maxTriesCount && pipelineDepth != AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(pipelineDepth)} set to {AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth} but it's set to {pipelineDepth?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter == _maxTriesCount && byteAlignment != AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment) + { + BugDetected = $"[BUG DETECTED] The very last try should be with {nameof(byteAlignment)} set to {AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment} but it's set to {byteAlignment?.ToString() ?? "(null)"} instead - something is wrong!"; + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, BugDetected, EGlobalErrorCode.Generic); + return; + } + + if (_tryCounter < _maxTriesCount) + { + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Error); + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, "error while uploading firmware blah blah", EGlobalErrorCode.Generic); // order + return; + } + + FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage: 20, averageThroughput: 10); + await Task.Delay(10); + FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage: 40, averageThroughput: 10); + await Task.Delay(10); + FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage: 60, averageThroughput: 10); + await Task.Delay(10); + FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage: 80, averageThroughput: 10); + await Task.Delay(10); + FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage: 100, averageThroughput: 10); + await Task.Delay(10); + } + + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Uploading, newState: EFirmwareInstallationState.Testing); + await Task.Delay(10); + + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Testing, newState: EFirmwareInstallationState.Confirming); + await Task.Delay(10); + + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Confirming, newState: EFirmwareInstallationState.Resetting); + await Task.Delay(10); + + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Resetting, newState: EFirmwareInstallationState.Complete); + }); + + return verdict; + + //00 simulating the state changes in a background thread is vital in order to simulate the async nature of the native uploader + } + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareInstaller.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareInstaller.cs index 1d343bb2..e22785cc 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareInstaller.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldCompleteSuccessfully_GivenGreenNativeFirmwareInstaller.cs @@ -21,7 +21,12 @@ public async Task InstallAsync_ShouldCompleteSuccessfully_GivenGreenNativeFirmwa using var eventsMonitor = firmwareInstaller.Monitor(); // Act - var work = new Func(() => firmwareInstaller.InstallAsync(new byte[] { 1, 2, 3 }, maxTriesCount: 1)); + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1, 2, 3], + maxTriesCount: 1, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); // Assert await work.Should().CompleteWithinAsync(4.Seconds()); @@ -30,7 +35,7 @@ public async Task InstallAsync_ShouldCompleteSuccessfully_GivenGreenNativeFirmwa mockedNativeFirmwareInstallerProxy.DisconnectCalled.Should().BeFalse(); //00 mockedNativeFirmwareInstallerProxy.BeginInstallationCalled.Should().BeTrue(); - eventsMonitor.OccurredEvents.Length.Should().Be(12); + eventsMonitor.OccurredEvents.Length.Should().Be(13); eventsMonitor .Should() @@ -66,6 +71,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -78,6 +84,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -85,6 +92,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( Task.Run(function: async () => //00 vital { + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Idle); await Task.Delay(10); StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Validating); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs index 76408798..15292531 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Events; @@ -21,7 +22,10 @@ public async Task InstallAsync_ShouldThrowAllFirmwareInstallationAttemptsFailedE // Act var work = new Func(() => firmwareInstaller.InstallAsync( - data: new byte[] { 1, 2, 3 }, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + data: [1, 2, 3], maxTriesCount: 2, sleepTimeBetweenRetriesInMs: 0 )); @@ -74,6 +78,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -86,6 +91,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -93,6 +99,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( Task.Run(async () => //00 vital { + StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Idle); await Task.Delay(100); StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Validating); @@ -104,8 +111,8 @@ public override EFirmwareInstallationVerdict BeginInstallation( StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Testing); await Task.Delay(100); - StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Error); // order - FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, "error while uploading firmware blah blah"); // order + StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Error); // order + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, "error while uploading firmware blah blah", EGlobalErrorCode.Generic); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs index f4ff1a6f..1ddceb23 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Events; @@ -20,7 +21,12 @@ public async Task InstallAsync_ShouldThrowAllFirmwareInstallationAttemptsFailedE using var eventsMonitor = firmwareInstaller.Monitor(); // Act - var work = new Func(() => firmwareInstaller.InstallAsync(data: new byte[] { 1, 2, 3 }, maxTriesCount: 1)); + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1, 2, 3], + maxTriesCount: 1, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); // Assert await work.Should() @@ -63,6 +69,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -75,6 +82,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -82,19 +90,20 @@ public override EFirmwareInstallationVerdict BeginInstallation( Task.Run(async () => //00 vital { + StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Idle); await Task.Delay(100); StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Validating); await Task.Delay(100); - + StateChangedAdvertisement(EFirmwareInstallationState.Validating, EFirmwareInstallationState.Uploading); await Task.Delay(100); StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Testing); await Task.Delay(100); - StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Error); // order - FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Confirming, EFirmwareInstallerFatalErrorType.Generic, "fatal error occurred"); // order + StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Error); // order + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Confirming, EFirmwareInstallerFatalErrorType.Generic, "fatal error occurred", EGlobalErrorCode.Generic); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs index 74b8ca50..c74ed713 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Events; @@ -20,7 +21,12 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationImageSwapTimeoutEx using var eventsMonitor = firmwareInstaller.Monitor(); // Act - var work = new Func(() => firmwareInstaller.InstallAsync(data: new byte[] { 1, 2, 3 }, maxTriesCount: 1)); + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1, 2, 3], + maxTriesCount: 1, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); // Assert await work.Should() @@ -61,6 +67,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -73,6 +80,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -80,6 +88,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( Task.Run(async () => //00 vital { + StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Idle); await Task.Delay(100); StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Validating); @@ -91,8 +100,8 @@ public override EFirmwareInstallationVerdict BeginInstallation( StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Testing); await Task.Delay(100); - StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Error); // order - FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Confirming, EFirmwareInstallerFatalErrorType.FirmwareImageSwapTimeout, "image swap timeout"); // order + StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Error); // order + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Confirming, EFirmwareInstallerFatalErrorType.FirmwareImageSwapTimeout, "image swap timeout", EGlobalErrorCode.Generic); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs index ad54c335..aad3e4d2 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs @@ -17,7 +17,12 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationInternalErrorExcep var firmwareInstaller = new McuMgr.FirmwareInstaller.FirmwareInstaller(mockedNativeFirmwareInstallerProxy); // Act - var work = new Func(() => firmwareInstaller.InstallAsync(new byte[] { 1 }, maxTriesCount: 1)); + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1], + maxTriesCount: 1, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); // Assert ( @@ -44,6 +49,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -56,6 +62,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs index f11735b9..f2b3895f 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs @@ -21,7 +21,14 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationTimeoutException_G using var eventsMonitor = firmwareInstaller.Monitor(); // Act - var work = new Func(() => firmwareInstaller.InstallAsync(new byte[] { 1 }, maxTriesCount: 1, timeoutInMs: 100)); + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1], + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp.", + + timeoutInMs: 100, + maxTriesCount: 1 + )); // Assert await work.Should() @@ -56,6 +63,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -68,6 +76,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -75,6 +84,9 @@ public override EFirmwareInstallationVerdict BeginInstallation( Task.Run(async () => //00 vital { + await Task.Delay(10); + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Idle); + await Task.Delay(10); StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Uploading); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs index 7969442e..f5b81077 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs @@ -32,7 +32,12 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationCancelledException firmwareInstaller.Cancel(); }); - var work = new Func(() => firmwareInstaller.InstallAsync(new byte[] { 1, 2, 3 }, maxTriesCount: 1)); + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1, 2, 3], + maxTriesCount: 1, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); // Assert await work.Should() @@ -86,6 +91,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -98,6 +104,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -116,6 +123,9 @@ public override EFirmwareInstallationVerdict BeginInstallation( if (_cancellationTokenSource.IsCancellationRequested) return; + await Task.Delay(100, _cancellationTokenSource.Token); + StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Idle); + await Task.Delay(100, _cancellationTokenSource.Token); StateChangedAdvertisement(EFirmwareInstallationState.Idle, EFirmwareInstallationState.Validating); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldTriggerEventIdenticalFirmwareCachedOnTargetDeviceDetected_GivenGreenNativeFileDownloaderThatSkipsIntoTesting.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldTriggerEventIdenticalFirmwareCachedOnTargetDeviceDetected_GivenGreenNativeFileDownloaderThatSkipsIntoTesting.cs index 439a45d5..953b3254 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldTriggerEventIdenticalFirmwareCachedOnTargetDeviceDetected_GivenGreenNativeFileDownloaderThatSkipsIntoTesting.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldTriggerEventIdenticalFirmwareCachedOnTargetDeviceDetected_GivenGreenNativeFileDownloaderThatSkipsIntoTesting.cs @@ -29,7 +29,12 @@ public async Task InstallAsync_ShouldTriggerEventIdenticalFirmwareCachedOnTarget using var eventsMonitor = firmwareInstaller.Monitor(); // Act - var work = new Func(() => firmwareInstaller.InstallAsync(new byte[] { 1, 2, 3 }, maxTriesCount: 1)); + var work = new Func(() => firmwareInstaller.InstallAsync( + data: [1, 2, 3], + maxTriesCount: 1, + hostDeviceModel: "foobar", + hostDeviceManufacturer: "acme corp." + )); // Assert await work.Should().CompleteWithinAsync(4.Seconds()); @@ -65,8 +70,7 @@ public MockedGreenNativeFirmwareInstallerProxySpy14( INativeFirmwareInstallerCallbacksProxy firmwareInstallerCallbacksProxy, int numberOfFirmwareUploadingEventsToEmitCount, ECachedFirmwareType cachedFirmwareTypeToEmulate - ) - : base(firmwareInstallerCallbacksProxy) + ) : base(firmwareInstallerCallbacksProxy) { _cachedFirmwareTypeToEmulate = cachedFirmwareTypeToEmulate; _numberOfFirmwareUploadingEventsToEmitCount = numberOfFirmwareUploadingEventsToEmitCount; @@ -77,6 +81,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -89,6 +94,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds @@ -96,6 +102,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( Task.Run(function: async () => //00 vital { + StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Idle); await Task.Delay(10); StateChangedAdvertisement(oldState: EFirmwareInstallationState.Idle, newState: EFirmwareInstallationState.Validating); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.cs index c5c57e48..533c2d3f 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.cs @@ -35,6 +35,7 @@ public virtual EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -68,8 +69,8 @@ public void StateChangedAdvertisement(EFirmwareInstallationState oldState, EFirm public void BusyStateChangedAdvertisement(bool busyNotIdle) => _firmwareInstallerCallbacksProxy.BusyStateChangedAdvertisement(busyNotIdle); //raises the actual event - public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage) - => _firmwareInstallerCallbacksProxy.FatalErrorOccurredAdvertisement(state, fatalErrorType, errorMessage); //raises the actual event + public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, EGlobalErrorCode globalErrorCode) + => _firmwareInstallerCallbacksProxy.FatalErrorOccurredAdvertisement(state, fatalErrorType, errorMessage, globalErrorCode); //raises the actual event public void FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput) => _firmwareInstallerCallbacksProxy.FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(progressPercentage, averageThroughput); //raises the actual event diff --git a/Laerdal.McuMgr.sln b/Laerdal.McuMgr.sln deleted file mode 100644 index 1b1732cf..00000000 --- a/Laerdal.McuMgr.sln +++ /dev/null @@ -1,69 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Misc", "_Misc", "{2459FC0F-B6EC-4C2C-BEA0-8534D539E124}" - ProjectSection(SolutionItems) = preProject - README.md = README.md - LICENSE = LICENSE - .gitignore = .gitignore - azure-pipelines.yml = azure-pipelines.yml - Laerdal.CreateNewReleaseInGithub.sh = Laerdal.Scripts\Laerdal.CreateNewReleaseInGithub.sh - Laerdal.Builder.targets = Laerdal.Scripts\Laerdal.Builder.targets - Laerdal.Version.sh = Laerdal.Scripts\Laerdal.Version.sh - global.json = global.json - Laerdal.SetupBuildEnvironment.sh = Laerdal.Scripts\Laerdal.SetupBuildEnvironment.sh - .github\workflows\github-actions.yml = .github\workflows\github-actions.yml - Laerdal.Scripts\Laerdal.GenerateSignAndUploadSbom.sh = Laerdal.Scripts\Laerdal.GenerateSignAndUploadSbom.sh - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Laerdal.McuMgr", "Laerdal.McuMgr\Laerdal.McuMgr.csproj", "{4E2952A5-394E-4184-8E12-F2D5342A43B2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Laerdal.McuMgr.Tests", "Laerdal.McuMgr.Tests\Laerdal.McuMgr.Tests.csproj", "{2112FF63-2823-428B-80EE-0ECEE476BA46}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{0D581C9E-E80B-4663-86A0-672A5B9843A8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Laerdal.McuMgr.Bindings.NetStandard", "Laerdal.McuMgr.Bindings.NetStandard\Laerdal.McuMgr.Bindings.NetStandard.csproj", "{84EEAAFB-5ED5-4697-9757-622FF332F44D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Laerdal.McuMgr.Bindings.Android", "Laerdal.McuMgr.Bindings.Android\Laerdal.McuMgr.Bindings.Android.csproj", "{C6A768F6-E649-4621-A9A8-099A7887BBBF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Laerdal.McuMgr.Bindings.iOS", "Laerdal.McuMgr.Bindings.iOS\Laerdal.McuMgr.Bindings.iOS.csproj", "{84273F19-16F7-4956-A4F6-74DF3F044F45}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Laerdal.McuMgr.Bindings.MacCatalyst", "Laerdal.McuMgr.Bindings.MacCatalyst\Laerdal.McuMgr.Bindings.MacCatalyst.csproj", "{DBB42900-156A-4233-9CBD-EF2D5D23278B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4E2952A5-394E-4184-8E12-F2D5342A43B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E2952A5-394E-4184-8E12-F2D5342A43B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E2952A5-394E-4184-8E12-F2D5342A43B2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E2952A5-394E-4184-8E12-F2D5342A43B2}.Release|Any CPU.Build.0 = Release|Any CPU - {E3C9ADE4-FF77-4615-984E-5C914537A350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E3C9ADE4-FF77-4615-984E-5C914537A350}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E3C9ADE4-FF77-4615-984E-5C914537A350}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E3C9ADE4-FF77-4615-984E-5C914537A350}.Release|Any CPU.Build.0 = Release|Any CPU - {2112FF63-2823-428B-80EE-0ECEE476BA46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2112FF63-2823-428B-80EE-0ECEE476BA46}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2112FF63-2823-428B-80EE-0ECEE476BA46}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2112FF63-2823-428B-80EE-0ECEE476BA46}.Release|Any CPU.Build.0 = Release|Any CPU - {84EEAAFB-5ED5-4697-9757-622FF332F44D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84EEAAFB-5ED5-4697-9757-622FF332F44D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84EEAAFB-5ED5-4697-9757-622FF332F44D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84EEAAFB-5ED5-4697-9757-622FF332F44D}.Release|Any CPU.Build.0 = Release|Any CPU - {C6A768F6-E649-4621-A9A8-099A7887BBBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6A768F6-E649-4621-A9A8-099A7887BBBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C6A768F6-E649-4621-A9A8-099A7887BBBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6A768F6-E649-4621-A9A8-099A7887BBBF}.Release|Any CPU.Build.0 = Release|Any CPU - {84273F19-16F7-4956-A4F6-74DF3F044F45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84273F19-16F7-4956-A4F6-74DF3F044F45}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84273F19-16F7-4956-A4F6-74DF3F044F45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84273F19-16F7-4956-A4F6-74DF3F044F45}.Release|Any CPU.Build.0 = Release|Any CPU - {DBB42900-156A-4233-9CBD-EF2D5D23278B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DBB42900-156A-4233-9CBD-EF2D5D23278B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DBB42900-156A-4233-9CBD-EF2D5D23278B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DBB42900-156A-4233-9CBD-EF2D5D23278B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {2112FF63-2823-428B-80EE-0ECEE476BA46} = {0D581C9E-E80B-4663-86A0-672A5B9843A8} - EndGlobalSection -EndGlobal diff --git a/Laerdal.McuMgr.sln.DotSettings b/Laerdal.McuMgr.sln.DotSettings deleted file mode 100644 index cf6cbeed..00000000 --- a/Laerdal.McuMgr.sln.DotSettings +++ /dev/null @@ -1,77 +0,0 @@ - - Named - True - Required - False - ExpressionBody - static public private protected internal file new abstract virtual sealed readonly override extern unsafe volatile async required - Remove - BaseClass - False - 1 - 0 - 1 - ALWAYS - ALWAYS - ALWAYS - NEVER - True - CHOP_IF_LONG - True - 999999 - CHOP_IF_LONG - CHOP_ALWAYS - /usr/local/share/dotnet/sdk/7.0.404/MSBuild.dll - /usr/local/share/dotnet/dotnet - 1048576 - 1 - /Applications/Xcode_14_2.app - NuGetPluginsThenRider - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True \ No newline at end of file diff --git a/Laerdal.McuMgr.slnx b/Laerdal.McuMgr.slnx new file mode 100644 index 00000000..c366a772 --- /dev/null +++ b/Laerdal.McuMgr.slnx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Laerdal.McuMgr.slnx.DotSettings.user b/Laerdal.McuMgr.slnx.DotSettings.user new file mode 100644 index 00000000..f9fbd1a2 --- /dev/null +++ b/Laerdal.McuMgr.slnx.DotSettings.user @@ -0,0 +1,84 @@ + + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Solution /> +</SessionState> + + True + True + True + Named + True + Required + False + ExpressionBody + static public private protected internal file new abstract virtual sealed readonly override extern unsafe volatile async required + Remove + BaseClass + False + 1 + 0 + 1 + ALWAYS + ALWAYS + ALWAYS + NEVER + True + CHOP_IF_LONG + True + 999999 + CHOP_IF_LONG + CHOP_ALWAYS + 1 + /Applications/Xcode_14_2.app + NuGetPluginsThenRider + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + \ No newline at end of file diff --git a/Laerdal.McuMgr/Droid/Common/HelpersAndroid.cs b/Laerdal.McuMgr/Droid/Common/HelpersAndroid.cs index 7b9945d8..557bef3b 100644 --- a/Laerdal.McuMgr/Droid/Common/HelpersAndroid.cs +++ b/Laerdal.McuMgr/Droid/Common/HelpersAndroid.cs @@ -7,12 +7,13 @@ static internal class HelpersAndroid { static public ELogLevel TranslateEAndroidLogLevel(string level) => level?.Trim().ToUpperInvariant() switch //derived from sl4j https://www.slf4j.org/api/org/apache/log4j/Level.html { + "TRACE" => ELogLevel.Trace, "DEBUG" => ELogLevel.Debug, - "TRACE" => ELogLevel.Verbose, + "VERBOSE" => ELogLevel.Verbose, "INFO" => ELogLevel.Info, - "FATAL" => ELogLevel.Error, "WARN" => ELogLevel.Warning, "ERROR" => ELogLevel.Error, + "FATAL" => ELogLevel.Error, _ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown log-level value") }; } diff --git a/Laerdal.McuMgr/Droid/DeviceResetter/DeviceResetter.cs b/Laerdal.McuMgr/Droid/DeviceResetter/DeviceResetter.cs index afc013d1..d00f0c03 100644 --- a/Laerdal.McuMgr/Droid/DeviceResetter/DeviceResetter.cs +++ b/Laerdal.McuMgr/Droid/DeviceResetter/DeviceResetter.cs @@ -49,20 +49,32 @@ public IDeviceResetterEventEmittable DeviceResetter //keep this to conform to th public EDeviceResetterState State => TranslateEAndroidDeviceResetterState(base.State ?? EAndroidDeviceResetterState.None); // ReSharper disable once UnusedMember.Local - private AndroidNativeDeviceResetterAdapterProxy(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + private AndroidNativeDeviceResetterAdapterProxy(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) { } - internal AndroidNativeDeviceResetterAdapterProxy(INativeDeviceResetterCallbacksProxy deviceResetterCallbacksProxy, Context context, BluetoothDevice bluetoothDevice) : base(context, bluetoothDevice) + internal AndroidNativeDeviceResetterAdapterProxy(INativeDeviceResetterCallbacksProxy deviceResetterCallbacksProxy, Context context, BluetoothDevice bluetoothDevice) + : base(context, bluetoothDevice) { _deviceResetterCallbacksProxy = deviceResetterCallbacksProxy ?? throw new ArgumentNullException(nameof(deviceResetterCallbacksProxy)); } - public override void FatalErrorOccurredAdvertisement(string errorMessage) + public EDeviceResetterInitializationVerdict BeginReset() { - base.FatalErrorOccurredAdvertisement(errorMessage); + return TranslateEAndroidDeviceResetterInitializationVerdict(base.BeginReset()); + } + + public override void FatalErrorOccurredAdvertisement(string errorMessage, int globalErrorCode) + { + base.FatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); - _deviceResetterCallbacksProxy?.FatalErrorOccurredAdvertisement(errorMessage); + FatalErrorOccurredAdvertisement(errorMessage, (EGlobalErrorCode) globalErrorCode); + } + + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) + { + _deviceResetterCallbacksProxy?.FatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); } public override void StateChangedAdvertisement(EAndroidDeviceResetterState oldState, EAndroidDeviceResetterState newState) @@ -75,7 +87,7 @@ public override void StateChangedAdvertisement(EAndroidDeviceResetterState oldSt ); } - //keep this method to adhere to the interface + //keep this override it is needed to conform to the interface public void StateChangedAdvertisement(EDeviceResetterState oldState, EDeviceResetterState newState) { _deviceResetterCallbacksProxy?.StateChangedAdvertisement( @@ -95,7 +107,7 @@ public override void LogMessageAdvertisement(string message, string category, st ); } - //keep this override its needed to conform to the interface + //keep this override it is needed to conform to the interface public void LogMessageAdvertisement(string message, string category, ELogLevel level) { _deviceResetterCallbacksProxy?.LogMessageAdvertisement(message, category, level); @@ -130,6 +142,26 @@ static private EDeviceResetterState TranslateEAndroidDeviceResetterState(EAndroi throw new ArgumentOutOfRangeException(nameof(state), state, "Unknown enum value"); } + + static private EDeviceResetterInitializationVerdict TranslateEAndroidDeviceResetterInitializationVerdict(EAndroidDeviceResetterInitializationVerdict verdict) + { + if (verdict == EAndroidDeviceResetterInitializationVerdict.Success) + { + return EDeviceResetterInitializationVerdict.Success; + } + + if (verdict == EAndroidDeviceResetterInitializationVerdict.FailedErrorUponCommencing) + { + return EDeviceResetterInitializationVerdict.FailedErrorUponCommencing; + } + + if (verdict == EAndroidDeviceResetterInitializationVerdict.FailedOtherResetAlreadyInProgress) + { + return EDeviceResetterInitializationVerdict.FailedOtherResetAlreadyInProgress; + } + + throw new ArgumentOutOfRangeException(nameof(verdict), verdict, "Unknown enum value"); + } } } } diff --git a/Laerdal.McuMgr/Droid/FileDownloader/FileDownloader.cs b/Laerdal.McuMgr/Droid/FileDownloader/FileDownloader.cs index a69ffc2d..b929d637 100644 --- a/Laerdal.McuMgr/Droid/FileDownloader/FileDownloader.cs +++ b/Laerdal.McuMgr/Droid/FileDownloader/FileDownloader.cs @@ -12,6 +12,7 @@ using Laerdal.McuMgr.FileDownloader.Contracts; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; namespace Laerdal.McuMgr.FileDownloader { @@ -90,23 +91,58 @@ private void CleanupInfrastructure() } } - #region commands - public new EFileDownloaderVerdict BeginDownload(string remoteFilePath) + #region commands + + public EFileDownloaderVerdict BeginDownload( + string remoteFilePath, + int? initialMtuSize = null // android only + ) + { + return TranslateFileDownloaderVerdict(base.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize ?? -1 + )); + } + + public bool TrySetContext(object context) //the parameter must be of type 'object' so that it wont cause problems in platforms other than android + { + var androidContext = context as Context ?? throw new ArgumentException($"Expected {nameof(Context)} to be an AndroidContext but got '{context?.GetType().Name ?? "null"}' instead", nameof(context)); + + return base.TrySetContext(androidContext); + } + + public bool TrySetBluetoothDevice(object bluetoothDevice) + { + var androidBluetoothDevice = bluetoothDevice as BluetoothDevice ?? throw new ArgumentException($"Expected {nameof(BluetoothDevice)} to be an AndroidBluetoothDevice but got '{bluetoothDevice?.GetType().Name ?? "null"}' instead", nameof(bluetoothDevice)); + + return base.TrySetBluetoothDevice(androidBluetoothDevice); + } + + public new bool TryInvalidateCachedTransport() { - return TranslateFileDownloaderVerdict(base.BeginDownload(remoteFilePath)); + return base.TryInvalidateCachedTransport(); } #endregion commands #region android callbacks -> csharp event emitters - - public override void FatalErrorOccurredAdvertisement(string resource, string errorMessage) + + public override void FatalErrorOccurredAdvertisement(string resource, string errorMessage, int globalErrorCode) { - base.FatalErrorOccurredAdvertisement(resource, errorMessage); + base.FatalErrorOccurredAdvertisement(resource, errorMessage, globalErrorCode); //just in case - _fileDownloaderCallbacksProxy?.FatalErrorOccurredAdvertisement(resource, errorMessage); + FatalErrorOccurredAdvertisement(resource, errorMessage, (EGlobalErrorCode) globalErrorCode); + } + + public void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EGlobalErrorCode globalErrorCode) + { + _fileDownloaderCallbacksProxy?.FatalErrorOccurredAdvertisement( + resource, + errorMessage, + globalErrorCode + ); } public override void LogMessageAdvertisement(string message, string category, string level, string resource) @@ -195,7 +231,12 @@ static private EFileDownloaderVerdict TranslateFileDownloaderVerdict(EAndroidFil { return EFileDownloaderVerdict.FailedInvalidSettings; } - + + if (verdict == EAndroidFileDownloaderVerdict.FailedErrorUponCommencing) + { + return EFileDownloaderVerdict.FailedErrorUponCommencing; + } + if (verdict == EAndroidFileDownloaderVerdict.FailedDownloadAlreadyInProgress) { return EFileDownloaderVerdict.FailedDownloadAlreadyInProgress; diff --git a/Laerdal.McuMgr/Droid/FileUploader/FileUploader.cs b/Laerdal.McuMgr/Droid/FileUploader/FileUploader.cs index c1b75c5c..9708e69c 100644 --- a/Laerdal.McuMgr/Droid/FileUploader/FileUploader.cs +++ b/Laerdal.McuMgr/Droid/FileUploader/FileUploader.cs @@ -39,7 +39,7 @@ static private INativeFileUploaderProxy ValidateArgumentsAndConstructProxy(Bluet ); } - private sealed class AndroidFileUploaderProxy : AndroidFileUploader, INativeFileUploaderProxy + private sealed class AndroidFileUploaderProxy : AndroidFileUploader, INativeFileUploaderProxy { private readonly INativeFileUploaderCallbacksProxy _fileUploaderCallbacksProxy; @@ -91,19 +91,38 @@ private void CleanupInfrastructure() // ignored } } - + public void CleanupResourcesOfLastUpload() { //nothing to do in android } - + #region commands - public new EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth, + int? byteAlignment, + int? initialMtuSize, + int? windowCapacity, + int? memoryAlignment + ) { - return TranslateFileUploaderVerdict(base.BeginUpload(remoteFilePath, data)); + return TranslateFileUploaderVerdict(base.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize ?? -1, + windowCapacity: windowCapacity ?? -1, + memoryAlignment: memoryAlignment ?? -1 + )); } + public new void Cancel(string reason = "") + { + base.Cancel(reason); + } + public bool TrySetContext(object context) //the parameter must be of type 'object' so that it wont cause problems in platforms other than android { var androidContext = context as Context ?? throw new ArgumentException($"Expected {nameof(Context)} to be an AndroidContext but got '{context?.GetType().Name ?? "null"}' instead", nameof(context)); @@ -124,26 +143,21 @@ public bool TrySetBluetoothDevice(object bluetoothDevice) } #endregion commands - + #region android callbacks -> csharp event emitters - public override void FatalErrorOccurredAdvertisement(string resource, string errorMessage, int mcuMgrErrorCode, int fileUploaderGroupReturnCode) + public override void FatalErrorOccurredAdvertisement(string resource, string errorMessage, int globalErrorCode) { - base.FatalErrorOccurredAdvertisement(resource, errorMessage, mcuMgrErrorCode, fileUploaderGroupReturnCode); //just in case + base.FatalErrorOccurredAdvertisement(resource, errorMessage, globalErrorCode); //just in case - FatalErrorOccurredAdvertisement(resource, errorMessage, (EMcuMgrErrorCode) mcuMgrErrorCode, (EFileUploaderGroupReturnCode) fileUploaderGroupReturnCode); + FatalErrorOccurredAdvertisement(resource, errorMessage, (EGlobalErrorCode) globalErrorCode); } - - public void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EMcuMgrErrorCode mcuMgrErrorCode, EFileUploaderGroupReturnCode fileUploaderGroupReturnCode) + + public void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EGlobalErrorCode globalErrorCode) { - _fileUploaderCallbacksProxy?.FatalErrorOccurredAdvertisement( - resource, - errorMessage, - mcuMgrErrorCode, - fileUploaderGroupReturnCode - ); + _fileUploaderCallbacksProxy?.FatalErrorOccurredAdvertisement(resource, errorMessage, globalErrorCode); } public override void LogMessageAdvertisement(string message, string category, string level, string resource) @@ -168,12 +182,19 @@ public void LogMessageAdvertisement(string message, string category, ELogLevel l resource: resource //essentially the remote filepath ); } + + public override void CancellingAdvertisement(string reason) + { + base.CancellingAdvertisement(reason); //just in case + + _fileUploaderCallbacksProxy?.CancellingAdvertisement(reason); + } - public override void CancelledAdvertisement() + public override void CancelledAdvertisement(string reason) { - base.CancelledAdvertisement(); //just in case + base.CancelledAdvertisement(reason); //just in case - _fileUploaderCallbacksProxy?.CancelledAdvertisement(); + _fileUploaderCallbacksProxy?.CancelledAdvertisement(reason); } public override void FileUploadedAdvertisement(string resource) @@ -229,17 +250,22 @@ static private EFileUploaderVerdict TranslateFileUploaderVerdict(EAndroidFileUpl { return EFileUploaderVerdict.Success; } + + if (verdict == EAndroidFileUploaderVerdict.FailedInvalidData) + { + return EFileUploaderVerdict.FailedInvalidData; + } if (verdict == EAndroidFileUploaderVerdict.FailedInvalidSettings) { return EFileUploaderVerdict.FailedInvalidSettings; } - if (verdict == EAndroidFileUploaderVerdict.FailedInvalidData) + if (verdict == EAndroidFileUploaderVerdict.FailedErrorUponCommencing) { - return EFileUploaderVerdict.FailedInvalidData; + return EFileUploaderVerdict.FailedErrorUponCommencing; } - + if (verdict == EAndroidFileUploaderVerdict.FailedOtherUploadAlreadyInProgress) { return EFileUploaderVerdict.FailedOtherUploadAlreadyInProgress; diff --git a/Laerdal.McuMgr/Droid/FirmwareEraser/FirmwareEraser.cs b/Laerdal.McuMgr/Droid/FirmwareEraser/FirmwareEraser.cs index 8447e90b..9797b86b 100644 --- a/Laerdal.McuMgr/Droid/FirmwareEraser/FirmwareEraser.cs +++ b/Laerdal.McuMgr/Droid/FirmwareEraser/FirmwareEraser.cs @@ -59,38 +59,54 @@ internal AndroidNativeFirmwareEraserAdapterProxy(INativeFirmwareEraserCallbacksP _nativeEraserCallbacksProxy = eraserCallbacksProxy ?? throw new ArgumentNullException(nameof(eraserCallbacksProxy)); //composition-over-inheritance } - + public IFirmwareEraserEventEmittable FirmwareEraser //keep this to conform to the interface { get => _nativeEraserCallbacksProxy!.FirmwareEraser; set => _nativeEraserCallbacksProxy!.FirmwareEraser = value; } + public EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex) + { + if (_nativeEraserCallbacksProxy == null) + throw new InvalidOperationException("The native firmware eraser is not initialized"); + + return TranslateEAndroidFirmwareEraserInitializationVerdict(base.BeginErasure(imageIndex)); + } + public override void StateChangedAdvertisement(EAndroidFirmwareEraserState oldState, EAndroidFirmwareEraserState newState) { base.StateChangedAdvertisement(oldState, newState); - - StateChangedAdvertisement(newState: TranslateEAndroidFirmwareEraserState(newState), oldState: TranslateEAndroidFirmwareEraserState(oldState)); + + StateChangedAdvertisement( + newState: TranslateEAndroidFirmwareEraserState(newState), + oldState: TranslateEAndroidFirmwareEraserState(oldState) + ); } - //keep this override its needed to conform to the interface + //keep this override it is needed to conform to the interface public void StateChangedAdvertisement(EFirmwareErasureState oldState, EFirmwareErasureState newState) { - _nativeEraserCallbacksProxy.StateChangedAdvertisement(newState: newState, oldState: oldState); + _nativeEraserCallbacksProxy?.StateChangedAdvertisement(newState: newState, oldState: oldState); } public override void BusyStateChangedAdvertisement(bool busyNotIdle) { base.BusyStateChangedAdvertisement(busyNotIdle); //just in case - _nativeEraserCallbacksProxy.BusyStateChangedAdvertisement(busyNotIdle); + _nativeEraserCallbacksProxy?.BusyStateChangedAdvertisement(busyNotIdle); } - public override void FatalErrorOccurredAdvertisement(string errorMessage) + public override void FatalErrorOccurredAdvertisement(string errorMessage, int globalErrorCode) { - base.FatalErrorOccurredAdvertisement(errorMessage); - - _nativeEraserCallbacksProxy.FatalErrorOccurredAdvertisement(errorMessage); + base.FatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); + + FatalErrorOccurredAdvertisement(errorMessage, (EGlobalErrorCode) globalErrorCode); + } + + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) + { + _nativeEraserCallbacksProxy?.FatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); } public override void LogMessageAdvertisement(string message, string category, string level) @@ -104,10 +120,10 @@ public override void LogMessageAdvertisement(string message, string category, st ); } - //keep this override its needed to conform to the interface + //keep this override it is needed to conform to the interface public void LogMessageAdvertisement(string message, string category, ELogLevel level) { - _nativeEraserCallbacksProxy.LogMessageAdvertisement(message, category, level); + _nativeEraserCallbacksProxy?.LogMessageAdvertisement(message, category, level); } // ReSharper disable once MemberCanBePrivate.Global @@ -140,6 +156,26 @@ static internal EFirmwareErasureState TranslateEAndroidFirmwareEraserState(EAndr throw new ArgumentOutOfRangeException(nameof(state), state, "Unknown enum value"); } + + static internal EFirmwareErasureInitializationVerdict TranslateEAndroidFirmwareEraserInitializationVerdict(EAndroidFirmwareEraserInitializationVerdict beginErasure) + { + if (beginErasure == EAndroidFirmwareEraserInitializationVerdict.Success) + { + return EFirmwareErasureInitializationVerdict.Success; + } + + if (beginErasure == EAndroidFirmwareEraserInitializationVerdict.FailedErrorUponCommencing) + { + return EFirmwareErasureInitializationVerdict.FailedErrorUponCommencing; + } + + if (beginErasure == EAndroidFirmwareEraserInitializationVerdict.FailedOtherErasureAlreadyInProgress) + { + return EFirmwareErasureInitializationVerdict.FailedOtherErasureAlreadyInProgress; + } + + throw new ArgumentOutOfRangeException(nameof(beginErasure), beginErasure, "Unknown enum value"); + } } } } diff --git a/Laerdal.McuMgr/Droid/FirmwareInstaller/FirmwareInstaller.cs b/Laerdal.McuMgr/Droid/FirmwareInstaller/FirmwareInstaller.cs index 64d19e07..4aa4d86b 100644 --- a/Laerdal.McuMgr/Droid/FirmwareInstaller/FirmwareInstaller.cs +++ b/Laerdal.McuMgr/Droid/FirmwareInstaller/FirmwareInstaller.cs @@ -104,16 +104,19 @@ public EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, // ignored in android it only affects ios - int? byteAlignment = null // ignored in android it only affects ios + int? byteAlignment = null + // ignored in android it only affects ios ) { var nativeVerdict = base.BeginInstallation( data: data, mode: TranslateFirmwareInstallationMode(mode), eraseSettings: eraseSettings ?? false, + initialMtuSize: initialMtuSize ?? -1, windowCapacity: windowCapacity ?? -1, memoryAlignment: memoryAlignment ?? -1, estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds ?? -1 @@ -128,23 +131,25 @@ public EFirmwareInstallationVerdict BeginInstallation( #region callbacks -> events - public override void FatalErrorOccurredAdvertisement(EAndroidFirmwareInstallationState state, EAndroidFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage) + public override void FatalErrorOccurredAdvertisement(EAndroidFirmwareInstallationState state, EAndroidFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, int globalErrorCode) { - base.FatalErrorOccurredAdvertisement(state, fatalErrorType, errorMessage); + base.FatalErrorOccurredAdvertisement(state, fatalErrorType, errorMessage, globalErrorCode); FatalErrorOccurredAdvertisement( state: TranslateEAndroidFirmwareInstallationState(state), errorMessage: errorMessage, - fatalErrorType: TranslateEAndroidFirmwareInstallerFatalErrorType(fatalErrorType) + fatalErrorType: TranslateEAndroidFirmwareInstallerFatalErrorType(fatalErrorType), + globalErrorCode: (EGlobalErrorCode) globalErrorCode ); } - public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage) //just to conform to the interface + public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, EGlobalErrorCode globalErrorCode) //just to conform to the interface { _firmwareInstallerCallbacksProxy?.FatalErrorOccurredAdvertisement( state: state, errorMessage: errorMessage, - fatalErrorType: fatalErrorType + fatalErrorType: fatalErrorType, + globalErrorCode: globalErrorCode ); } @@ -291,6 +296,11 @@ static private EFirmwareInstallerFatalErrorType TranslateEAndroidFirmwareInstall return EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut; } + if (fatalErrorType == EAndroidFirmwareInstallerFatalErrorType.FailedInstallationAlreadyInProgress) + { + return EFirmwareInstallerFatalErrorType.FailedInstallationAlreadyInProgress; + } + throw new ArgumentOutOfRangeException(nameof(fatalErrorType), fatalErrorType, "Unknown enum value"); } diff --git a/Laerdal.McuMgr/Laerdal.McuMgr.csproj b/Laerdal.McuMgr/Laerdal.McuMgr.csproj index e2bcd4d7..0d24ba5a 100644 --- a/Laerdal.McuMgr/Laerdal.McuMgr.csproj +++ b/Laerdal.McuMgr/Laerdal.McuMgr.csproj @@ -2,10 +2,10 @@ - true - true - true - true + true + true + true + true @@ -13,8 +13,8 @@ $(TargetFrameworks)netstandard2.1; $(TargetFrameworks)net8.0-android; - $(TargetFrameworks)net8.0-ios11; - $(TargetFrameworks)net8.0-maccatalyst + $(TargetFrameworks)net8.0-ios11; + $(TargetFrameworks)net8.0-maccatalyst true true @@ -29,8 +29,24 @@ true true - true + true + + true + + + + $(Laerdal_Bindings_iOS___DotnetTargetPlatformVersion) + 17.0 + $(Laerdal_Bindings_Android___DotnetTargetPlatformVersion) + 34 + $(Laerdal_Bindings_MacCatalyst___DotnetTargetPlatformVersion) + 17.0 + + 11.0 + 21 + 13.1 + Library bin\ 9 @@ -55,10 +71,10 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 - 1.0.1152.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 + 1.0.1177.0 $(PackageId) $(Authors) @@ -97,26 +113,29 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -131,7 +150,6 @@ armeabi-v7a;arm64-v8a - @@ -149,21 +167,21 @@ - + - + - + - + diff --git a/Laerdal.McuMgr/Shared/Common/Constants/Android.FailSafeBleConnectionSettings.cs b/Laerdal.McuMgr/Shared/Common/Constants/Android.FailSafeBleConnectionSettings.cs new file mode 100644 index 00000000..b075a9cc --- /dev/null +++ b/Laerdal.McuMgr/Shared/Common/Constants/Android.FailSafeBleConnectionSettings.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Laerdal.McuMgr.Common.Constants +{ + public readonly struct AndroidTidbits + { + /// List of known problematic android-devices that have issues with BLE connection stability and have to use fail-safe ble settings to work.

+ /// Inspired by + static public HashSet<(string DeviceModel, string Manufacturer)> KnownProblematicDevices { get; } = new (string DeviceModel, string Manufacturer)[] + { + ("moto g20", "Motorola"), + ("moto e20", "Motorola"), + ("moto e30", "Motorola"), + ("moto e32", "Motorola"), + ("moto e40", "Motorola"), + + ("Nokia G21", "Nokia"), + ("Nokia G11", "Nokia"), + ("Nokia T20", "Nokia"), + + ("RMX3261", "Realme"), //C21Y + ("RMX3262", "Realme"), //C21Y + ("RMX3265", "Realme"), //C25Y + ("RMX3269", "Realme"), //C25Y + ("RMP2105", "Realme"), //Pad Mini + ("RMP2106", "Realme"), //Pad Mini + + ("Infinix X675", "Infinix"), //Hot 11 2022 + + ("Wildfire E2 plus", "HTC"), + + ("IN_2b", "Micromax"), + ("IN_2c", "Micromax"), + + ("SM-X200", "Samsung"), //Galaxy Tab A8 + } + .Select(x => (DeviceModel: x.DeviceModel.Trim().ToLowerInvariant(), Manufacturer: x.Manufacturer.Trim().ToLowerInvariant())) //vital + .ToHashSet(); + + /// + /// Failsafe settings for the BLE connection used in Androids to perform various operations: installing firmware, resetting the device, erasing firmwares, uploading files, + /// downloading files. These settings are enforced automagically when the ble connection turns out to be unstable and unreliable during the aforementioned operations. + /// The settings are editable can be adjusted to fit future needs. + /// + public readonly struct BleConnectionFailsafeSettings + { + public readonly struct ForUploading + { + static public int InitialMtuSize { get; set; } = 23; + static public int WindowCapacity { get; set; } = 1; + static public int MemoryAlignment { get; set; } = 1; + } + + public readonly struct ForDownloading + { + static public int InitialMtuSize { get; set; } = 50; //00 + //static public int WindowCapacity { get; set; }= 1; //10 + + //00 oddly enough when it comes to downloading using a value of 23 is not supported even by healthy devices so we have to use a greater value it is worth noting + // however that even among healthy devices the lowest value supported varies some can go as low as 25 while others only as low as 30 go figure + // + //10 window capacity could be supported in the future currently its not support though https://github.com/NordicSemiconductor/Android-nRF-Connect-Device-Manager/issues/188#issuecomment-2391146897 + } + } + } +} diff --git a/Laerdal.McuMgr/Shared/Common/Constants/Apple.FailSafeBleConnectionSettings.cs b/Laerdal.McuMgr/Shared/Common/Constants/Apple.FailSafeBleConnectionSettings.cs new file mode 100644 index 00000000..c92b75cc --- /dev/null +++ b/Laerdal.McuMgr/Shared/Common/Constants/Apple.FailSafeBleConnectionSettings.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Laerdal.McuMgr.Common.Constants +{ + public readonly struct AppleTidbits + { + // ReSharper disable once CollectionNeverUpdated.Global + /// List of known problematic android-devices that have issues with BLE connection stability and have to use fail-safe ble settings to work.

+ /// Inspired by
+ static public HashSet<(string DeviceModel, string Manufacturer)> KnownProblematicDevices { get; } = new (string DeviceModel, string Manufacturer)[] + { + // ("iPhone6", "Apple"), // just a placeholder - no apple devices are known to have issues with BLE connection stability at the time of this writing + } + .Select(x => (DeviceModel: x.DeviceModel.Trim().ToLowerInvariant(), Manufacturer: x.Manufacturer.Trim().ToLowerInvariant())) + .ToHashSet(); + + /// + /// Failsafe settings for the BLE connection used in Apple platforms (iOS / MacCatalyst) to perform various operations: installing firmware, resetting the device, + /// erasing firmwares, uploading files, downloading files. These settings are enforced automagically when the ble connection turns out to be unstable and unreliable + /// during the aforementioned operations. The settings are editable can be adjusted to fit future needs. + /// + public readonly struct BleConnectionFailsafeSettings + { + public readonly struct ForUploading + { + static public int PipelineDepth { get; set; } = 1; + static public int ByteAlignment { get; set; } = 1; + } + + // public readonly struct ForDownloading //there are currently no apple devices that have issues with BLE connection stability when downloading + // { + // // static public int PipelineDepth { get; set; } = null; //not applicable to downloads + // // static public int ByteAlignment { get; set; } = 1; //not applicable to downloads + // } + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/Common/Enums/EGlobalErrorCode.cs b/Laerdal.McuMgr/Shared/Common/Enums/EGlobalErrorCode.cs new file mode 100644 index 00000000..07b0f777 --- /dev/null +++ b/Laerdal.McuMgr/Shared/Common/Enums/EGlobalErrorCode.cs @@ -0,0 +1,109 @@ +namespace Laerdal.McuMgr.Common.Enums +{ + public enum EGlobalErrorCode //@formatter:off https://github.com/NordicSemiconductor/Android-nRF-Connect-Device-Manager/issues/201#issuecomment-2440126159 + { + Unset = -99, //this is our own to mark that we haven't received any error code from the device + Generic = -1, //in case the underlying native code receives an exception other than io.runtime.mcumgr.exception.McuMgrErrorException + + // must mirror mcumgr-core/src/main/java/io/runtime/mcumgr/exception/McuMgrErrorException.java + McuMgrErrorBeforeSmpV2_Ok = 000, + McuMgrErrorBeforeSmpV2_Unknown = 001, //when uploading files to the device this error code means that the target file-path has one or more non-existent directories in it + McuMgrErrorBeforeSmpV2_NoMemory = 002, + McuMgrErrorBeforeSmpV2_InValue = 003, + McuMgrErrorBeforeSmpV2_Timeout = 004, + McuMgrErrorBeforeSmpV2_NoEntry = 005, + McuMgrErrorBeforeSmpV2_BadState = 006, + McuMgrErrorBeforeSmpV2_TooLarge = 007, + McuMgrErrorBeforeSmpV2_NotSupported = 008, + McuMgrErrorBeforeSmpV2_Corrupt = 009, + McuMgrErrorBeforeSmpV2_Busy = 010, + McuMgrErrorBeforeSmpV2_AccessDenied = 011, + McuMgrErrorBeforeSmpV2_ProtocolVersionTooOld = 012, + McuMgrErrorBeforeSmpV2_ProtocolVersionTooNew = 013, + McuMgrErrorBeforeSmpV2_PerUser = 256, + + SubSystemDefault_Ok = 1000, + SubSystemDefault_Unknown = 1001, + SubSystemDefault_InvalidFormat = 1002, + SubSystemDefault_QueryYieldsNoAnswer = 1003, + + SubSystemImage_Ok = 2000, + SubSystemImage_Unknown = 2001, + SubSystemImage_FlashConfigQueryFail = 2002, + SubSystemImage_NoImage = 2003, + SubSystemImage_NoTlvs = 2004, + SubSystemImage_InvalidTlv = 2005, + SubSystemImage_TlvMultipleHashesFound = 2006, + SubSystemImage_TlvInvalidSize = 2007, + SubSystemImage_HashNotFound = 2008, + SubSystemImage_NoFreeSlot = 2009, + SubSystemImage_FlashOpenFailed = 2010, + SubSystemImage_FlashReadFailed = 2011, + SubSystemImage_FlashWriteFailed = 2012, + SubSystemImage_FlashEraseFailed = 2013, + SubSystemImage_InvalidSlot = 2014, + SubSystemImage_NoFreeMemory = 2015, + SubSystemImage_FlashContextAlreadySet = 2016, + SubSystemImage_FlashContextNotSet = 2017, + SubSystemImage_FlashAreaDeviceNull = 2018, + SubSystemImage_InvalidPageOffset = 2019, + SubSystemImage_InvalidOffset = 2020, + SubSystemImage_InvalidLength = 2021, + SubSystemImage_InvalidImageHeader = 2022, + SubSystemImage_InvalidImageHeaderMagic = 2023, + SubSystemImage_InvalidHash = 2024, + SubSystemImage_InvalidFlashAddress = 2025, + SubSystemImage_VersionGetFailed = 2026, + SubSystemImage_CurrentVersionIsNewer = 2027, + SubSystemImage_ImageAlreadyPending = 2028, + SubSystemImage_InvalidImageVectorTable = 2029, + SubSystemImage_InvalidImageTooLarge = 2030, + SubSystemImage_InvalidImageDataOverrun = 2031, + SubSystemImage_ImageConfirmationDenied = 2032, + SubSystemImage_ImageSettingTestToActiveDenied = 2033, + + SubSystemStats_Ok = 3000, + SubSystemStats_Unknown = 3001, + SubSystemStats_InvalidGroupName = 3002, + SubSystemStats_InvalidStatName = 3003, + SubSystemStats_InvalidStatSize = 3004, + SubSystemStats_WalkAborted = 3005, + + SubSystemSettings_Ok = 4000, + SubSystemSettings_Unknown = 4001, + SubSystemSettings_KeyTooLong = 4002, + SubSystemSettings_KeyNotFound = 4003, + SubSystemSettings_ReadNotSupported = 4004, + SubSystemSettings_RootKeyNotFound = 4005, + SubSystemSettings_WriteNotSupported = 4006, + SubSystemSettings_DeleteNotSupported = 4007, + + // int GROUP_LOGS = 4 // these group-ids are specific to nordic and are not supported by zephyr + // int GROUP_CRASH = 5 // so nordic did not migrate these to smp v2 so they do not support + // int GROUP_SPLIT = 6 // group-errors at least not out-of-the-box + // int GROUP_RUN = 7 + + SubSystemFilesystem_Ok = 9000, + SubSystemFilesystem_Unknown = 9001, + SubSystemFilesystem_InvalidName = 9002, + SubSystemFilesystem_NotFound = 9003, + SubSystemFilesystem_IsDirectory = 9004, + SubSystemFilesystem_OpenFailed = 9005, + SubSystemFilesystem_SeekFailed = 9006, + SubSystemFilesystem_ReadFailed = 9007, + SubSystemFilesystem_TruncateFailed = 9008, + SubSystemFilesystem_DeleteFailed = 9009, + SubSystemFilesystem_WriteFailed = 9010, + SubSystemFilesystem_OffsetNotValid = 9011, + SubSystemFilesystem_OffsetLargerThanFile = 9012, + SubSystemFilesystem_ChecksumHashNotFound = 9013, + SubSystemFilesystem_MountPointNotFound = 9014, + SubSystemFilesystem_ReadOnlyFilesystem = 9015, + SubSystemFilesystem_FileEmpty = 9016, + + SubSystemShell_Ok = 10000, + SubSystemShell_Unknown = 10001, + SubSystemShell_CommandTooLong = 10002, + SubSystemShell_EmptyCommand = 10003, + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/Common/Enums/ELogLevel.cs b/Laerdal.McuMgr/Shared/Common/Enums/ELogLevel.cs index 84d903cd..0efe9ae8 100644 --- a/Laerdal.McuMgr/Shared/Common/Enums/ELogLevel.cs +++ b/Laerdal.McuMgr/Shared/Common/Enums/ELogLevel.cs @@ -2,6 +2,7 @@ namespace Laerdal.McuMgr.Common.Enums { public enum ELogLevel { + Trace = 0, Debug = 0, Verbose = 1, Info = 2, diff --git a/Laerdal.McuMgr/Shared/Common/Enums/EMcuMgrErrorCode.cs b/Laerdal.McuMgr/Shared/Common/Enums/EMcuMgrErrorCode.cs deleted file mode 100644 index 6751bc1e..00000000 --- a/Laerdal.McuMgr/Shared/Common/Enums/EMcuMgrErrorCode.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Laerdal.McuMgr.Common.Enums -{ - public enum EMcuMgrErrorCode // must mirror io.runtime.mcumgr.McuMgrErrorCode @formatter:off - { - Unset = -99, //this is our own to mark that we haven't received any error code from the device - Ok = 000, - Unknown = 001, - NoMemory = 002, - InValue = 003, - Timeout = 004, - NoEntry = 005, - BadState = 006, - TooLarge = 007, - NotSupported = 008, - Corrupt = 009, - Busy = 010, - AccessDenied = 011, - PerUser = 256, - } // @formatter:on -} diff --git a/Laerdal.McuMgr/Shared/Common/Exceptions/UnauthorizedException.cs b/Laerdal.McuMgr/Shared/Common/Exceptions/UnauthorizedException.cs index 0aa43690..30d9d675 100644 --- a/Laerdal.McuMgr/Shared/Common/Exceptions/UnauthorizedException.cs +++ b/Laerdal.McuMgr/Shared/Common/Exceptions/UnauthorizedException.cs @@ -3,13 +3,15 @@ namespace Laerdal.McuMgr.Common.Exceptions { - public class UnauthorizedException : Exception, IMcuMgrException //todo get rid of this once we refactor all classes to use their own unauthorized exceptions + public class UnauthorizedException : Exception, IMcuMgrException { public string Resource { get; } = ""; + public EGlobalErrorCode GlobalErrorCode { get; } - public UnauthorizedException(string nativeErrorMessage) - : base($"Operation denied because it's not authorized: '{nativeErrorMessage}'") + public UnauthorizedException(string nativeErrorMessage, EGlobalErrorCode globalErrorCode) + : base($"Operation denied because it's not authorized: '{nativeErrorMessage}' (globalErrorCode={globalErrorCode})") { + GlobalErrorCode = globalErrorCode; } public UnauthorizedException(string nativeErrorMessage, string resource) diff --git a/Laerdal.McuMgr/Shared/Common/Helpers/ConnectionSettingsHelpers.cs b/Laerdal.McuMgr/Shared/Common/Helpers/ConnectionSettingsHelpers.cs new file mode 100644 index 00000000..a3cffe7b --- /dev/null +++ b/Laerdal.McuMgr/Shared/Common/Helpers/ConnectionSettingsHelpers.cs @@ -0,0 +1,95 @@ +using Laerdal.McuMgr.Common.Constants; + +namespace Laerdal.McuMgr.Common.Helpers +{ + static internal class ConnectionSettingsHelpers + { + static public (int? byteAlignment, int? pipelineDepth, int? initialMtuSize, int? windowCapacity, int? memoryAlignment)? GetFailsafeConnectionSettingsIfConnectionProvedToBeUnstable( + bool uploadingNotDownloading, + int triesCount, + int maxTriesCount, + int suspiciousTransportFailuresCount + ) + { + var isConnectionTooUnstableForUploading = triesCount >= 2 && (triesCount == maxTriesCount || triesCount >= 3 && suspiciousTransportFailuresCount >= 2); + if (!isConnectionTooUnstableForUploading) + return null; + + var byteAlignment = uploadingNotDownloading // ios + maccatalyst + ? AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment + : (int?)null; //byteAlignment is not applicable for downloads + var pipelineDepth = uploadingNotDownloading // ios + maccatalyst + ? AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth + : (int?)null; //pipelineDepth is not applicable for downloads + + var initialMtuSize = uploadingNotDownloading //android when noticing persistent failures when uploading/downloading we + ? AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize // resort to forcing the most failsafe settings we know of just in case + : AndroidTidbits.BleConnectionFailsafeSettings.ForDownloading.InitialMtuSize; // we manage to salvage this situation (works with SamsungA8 android tablets) + var windowCapacity = uploadingNotDownloading + ? AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity + : (int?)null; //window-capacity is not applicable for downloads + var memoryAlignment = uploadingNotDownloading + ? AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment + : (int?)null; //memory-alignment is not applicable for downloads + + return (byteAlignment: byteAlignment, pipelineDepth: pipelineDepth, initialMtuSize: initialMtuSize, windowCapacity: windowCapacity, memoryAlignment: memoryAlignment); + } + + static public (int? byteAlignment, int? pipelineDepth, int? initialMtuSize, int? windowCapacity, int? memoryAlignment)? GetFailSafeConnectionSettingsIfHostDeviceIsProblematic( + bool uploadingNotDownloading, + string hostDeviceModel, + string hostDeviceManufacturer, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null + ) + { + hostDeviceModel = (hostDeviceModel ?? "").Trim().ToLowerInvariant(); + hostDeviceManufacturer = (hostDeviceManufacturer ?? "").Trim().ToLowerInvariant(); + + var isUsingDefaultAppleSettings = pipelineDepth == null && byteAlignment == null; + if (isUsingDefaultAppleSettings && AppleTidbits.KnownProblematicDevices.Contains((DeviceModel: hostDeviceModel, Manufacturer: hostDeviceManufacturer))) + { + return uploadingNotDownloading + ? ( //uploading + byteAlignment: AppleTidbits.BleConnectionFailsafeSettings.ForUploading.ByteAlignment, + pipelineDepth: AppleTidbits.BleConnectionFailsafeSettings.ForUploading.PipelineDepth, + initialMtuSize: null, //only applies to android + windowCapacity: null, //only applies to android + memoryAlignment: null //only applies to android + ) + : ( //downloading + byteAlignment: null, //placeholder value currently there are no known apple devices that have issues with BLE connection stability + pipelineDepth: null, //placeholder value currently there are no known apple devices that have issues with BLE connection stability + initialMtuSize: null, //only applies to android + windowCapacity: null, //only applies to android + memoryAlignment: null //only applies to android + ); + } + + var isUsingDefaultAndroidSettings = initialMtuSize == null && windowCapacity == null && memoryAlignment == null; + if (isUsingDefaultAndroidSettings && AndroidTidbits.KnownProblematicDevices.Contains((DeviceModel: hostDeviceModel, Manufacturer: hostDeviceManufacturer))) + { + return uploadingNotDownloading + ? ( //uploading + byteAlignment: null, //only applies to apple + pipelineDepth: null, //only applies to apple + initialMtuSize: AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.InitialMtuSize, + windowCapacity: AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.WindowCapacity, + memoryAlignment: AndroidTidbits.BleConnectionFailsafeSettings.ForUploading.MemoryAlignment + ) + : ( //downloading + byteAlignment: null, //only applies to apple + pipelineDepth: null, //only applies to apple + initialMtuSize: AndroidTidbits.BleConnectionFailsafeSettings.ForDownloading.InitialMtuSize, + windowCapacity: null, // currently it doesnt apply to android downloads but nordic might consider adding it in the future + memoryAlignment: null // doesnt apply to android downloads + ); + } + + return null; + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/Common/Helpers/RemoteFilePathHelpers.cs b/Laerdal.McuMgr/Shared/Common/Helpers/RemoteFilePathHelpers.cs index 5b033a44..e3febf1b 100644 --- a/Laerdal.McuMgr/Shared/Common/Helpers/RemoteFilePathHelpers.cs +++ b/Laerdal.McuMgr/Shared/Common/Helpers/RemoteFilePathHelpers.cs @@ -13,13 +13,17 @@ static public void ValidateRemoteFilePathsWithDataBytes(IDictionary(T payloadForUploading) + { + if (payloadForUploading == null) + throw new ArgumentException("Bytes set to null!"); + } + static internal void ValidateRemoteFilePaths(IEnumerable remoteFilePaths) { remoteFilePaths = remoteFilePaths ?? throw new ArgumentNullException(nameof(remoteFilePaths)); @@ -36,7 +40,7 @@ static internal void ValidateRemoteFilePath(string remoteFilePath) throw new ArgumentException($"The {nameof(remoteFilePath)} parameter is dud!"); remoteFilePath = remoteFilePath.Trim(); //order - if (remoteFilePath.EndsWith("/")) //00 + if (remoteFilePath.EndsWith('/')) //00 throw new ArgumentException($"The given {nameof(remoteFilePath)} points to a directory not a file!"); if (remoteFilePath.Contains('\r') || remoteFilePath.Contains('\n') || remoteFilePath.Contains('\f')) //order @@ -85,7 +89,7 @@ static internal string SanitizeRemoteFilePath(string remoteFilePath) { remoteFilePath = remoteFilePath?.Trim() ?? ""; - remoteFilePath = remoteFilePath.StartsWith("/") //10 + remoteFilePath = remoteFilePath.StartsWith('/') //10 ? remoteFilePath : $"/{remoteFilePath}"; diff --git a/Laerdal.McuMgr/Shared/Common/Helpers/StreamExtensions.cs b/Laerdal.McuMgr/Shared/Common/Helpers/StreamExtensions.cs index 65f36e45..c5cf75eb 100644 --- a/Laerdal.McuMgr/Shared/Common/Helpers/StreamExtensions.cs +++ b/Laerdal.McuMgr/Shared/Common/Helpers/StreamExtensions.cs @@ -16,7 +16,7 @@ static internal async Task ReadBytesAsync(this Stream stream, int maxByt var result = memoryStream.ToArray(); if (disposeStream) - stream.Dispose(); //todo use await stream.DisposeAsync() here when we upgrade to .net 8 + await stream.DisposeAsync(); return result; } @@ -32,7 +32,7 @@ static internal async Task ReadBytesAsync(this Stream stream, int maxByt ); if (disposeStream) - stream.Dispose(); //todo use await stream.DisposeAsync() here when we upgrade to .net 8 + await stream.DisposeAsync(); return tempMemoryStream.ToArray(); } diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Enums/EDeviceResetterInitializationVerdict.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Enums/EDeviceResetterInitializationVerdict.cs new file mode 100644 index 00000000..4eca91d3 --- /dev/null +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Enums/EDeviceResetterInitializationVerdict.cs @@ -0,0 +1,9 @@ +namespace Laerdal.McuMgr.DeviceResetter.Contracts.Enums +{ + public enum EDeviceResetterInitializationVerdict + { + Success = 0, + FailedErrorUponCommencing = 1, + FailedOtherResetAlreadyInProgress = 2, + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Enums/EDeviceResetterState.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Enums/EDeviceResetterState.cs index 846a6ebb..52d2165a 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Enums/EDeviceResetterState.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Enums/EDeviceResetterState.cs @@ -6,6 +6,6 @@ public enum EDeviceResetterState Idle = 1, Resetting = 2, Complete = 3, - Failed = 4 + Failed = 4, } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Events/FatalErrorOccurredEventArgs.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Events/FatalErrorOccurredEventArgs.cs index e885d19e..43fd0f95 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Events/FatalErrorOccurredEventArgs.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Events/FatalErrorOccurredEventArgs.cs @@ -1,6 +1,7 @@ // ReSharper disable MemberCanBePrivate.Global // ReSharper disable ClassNeverInstantiated.Global +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Events; namespace Laerdal.McuMgr.DeviceResetter.Contracts.Events @@ -8,10 +9,12 @@ namespace Laerdal.McuMgr.DeviceResetter.Contracts.Events public readonly struct FatalErrorOccurredEventArgs : IMcuMgrEventArgs { public string ErrorMessage { get; } - - public FatalErrorOccurredEventArgs(string errorMessage) + public EGlobalErrorCode GlobalErrorCode { get; } + + public FatalErrorOccurredEventArgs(string errorMessage, EGlobalErrorCode globalErrorCode) { ErrorMessage = errorMessage; + GlobalErrorCode = globalErrorCode; } } } diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Exceptions/DeviceResetterErroredOutException.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Exceptions/DeviceResetterErroredOutException.cs index d545169e..c1663b00 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Exceptions/DeviceResetterErroredOutException.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Exceptions/DeviceResetterErroredOutException.cs @@ -1,11 +1,15 @@ using System; +using Laerdal.McuMgr.Common.Enums; namespace Laerdal.McuMgr.DeviceResetter.Contracts.Exceptions { public class DeviceResetterErroredOutException : Exception, IDeviceResetterException { - public DeviceResetterErroredOutException(string errorMessage) : base($"An error occurred while resetting/rebooting the device: '{errorMessage}'") + public EGlobalErrorCode GlobalErrorCode { get; } = EGlobalErrorCode.Unset; + + public DeviceResetterErroredOutException(string errorMessage, EGlobalErrorCode globalErrorCode) : base($"An error occurred while resetting/rebooting the device: '{errorMessage}'") { + GlobalErrorCode = globalErrorCode; } public DeviceResetterErroredOutException(string errorMessage, Exception innerException) : base($"An error occurred while resetting/rebooting the device: '{errorMessage}'", innerException) diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/IDeviceResetterCommandable.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/IDeviceResetterCommandable.cs index df4ee227..1509d665 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/IDeviceResetterCommandable.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/IDeviceResetterCommandable.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Laerdal.McuMgr.DeviceResetter.Contracts.Enums; namespace Laerdal.McuMgr.DeviceResetter.Contracts { @@ -11,7 +12,7 @@ public interface IDeviceResetterCommandable Task ResetAsync(int timeoutInMs = -1); /// Starts the resetting process. Basically reboots the device - it doesn't delete any of the firmware or the configuration. - void BeginReset(); + EDeviceResetterInitializationVerdict BeginReset(); /// Drops the active bluetooth-connection to the Zephyr device. void Disconnect(); diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCallbacksProxy.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCallbacksProxy.cs index 8e379c36..a99ed3b7 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCallbacksProxy.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCallbacksProxy.cs @@ -11,6 +11,6 @@ internal interface INativeDeviceResetterCallbacksProxy public void StateChangedAdvertisement(EDeviceResetterState oldState, EDeviceResetterState newState); - public void FatalErrorOccurredAdvertisement(string errorMessage); + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode); } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCommandableProxy.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCommandableProxy.cs index 3593da6a..a7b473b9 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCommandableProxy.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterCommandableProxy.cs @@ -1,8 +1,10 @@ -namespace Laerdal.McuMgr.DeviceResetter.Contracts.Native +using Laerdal.McuMgr.DeviceResetter.Contracts.Enums; + +namespace Laerdal.McuMgr.DeviceResetter.Contracts.Native { internal interface INativeDeviceResetterCommandableProxy { void Disconnect(); - void BeginReset(); + EDeviceResetterInitializationVerdict BeginReset(); } } diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterProxy.cs b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterProxy.cs index d1af2921..ff3a5bc2 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterProxy.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/Contracts/Native/INativeDeviceResetterProxy.cs @@ -1,6 +1,9 @@ namespace Laerdal.McuMgr.DeviceResetter.Contracts.Native { - internal interface INativeDeviceResetterProxy : INativeDeviceResetterQueryableProxy, INativeDeviceResetterCommandableProxy, INativeDeviceResetterCallbacksProxy + internal interface INativeDeviceResetterProxy : + INativeDeviceResetterQueryableProxy, + INativeDeviceResetterCommandableProxy, + INativeDeviceResetterCallbacksProxy { } } diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs b/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs index a4d42557..ff40e62f 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs @@ -18,8 +18,6 @@ namespace Laerdal.McuMgr.DeviceResetter /// public partial class DeviceResetter : IDeviceResetter, IDeviceResetterEventEmittable { - - //this sort of approach proved to be necessary for our testsuite to be able to effectively mock away the INativeDeviceResetterProxy internal class GenericNativeDeviceResetterCallbacksProxy : INativeDeviceResetterCallbacksProxy { @@ -39,8 +37,8 @@ public void StateChangedAdvertisement(EDeviceResetterState oldState, EDeviceRese oldState: oldState )); - public void FatalErrorOccurredAdvertisement(string errorMessage) - => DeviceResetter?.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(errorMessage)); + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) + => DeviceResetter?.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(errorMessage, globalErrorCode)); } private readonly INativeDeviceResetterProxy _nativeDeviceResetterProxy; @@ -56,7 +54,13 @@ internal DeviceResetter(INativeDeviceResetterProxy nativeDeviceResetterProxy) public string LastFatalErrorMessage => _nativeDeviceResetterProxy?.LastFatalErrorMessage; public void Disconnect() => _nativeDeviceResetterProxy?.Disconnect(); - public void BeginReset() => _nativeDeviceResetterProxy?.BeginReset(); + public EDeviceResetterInitializationVerdict BeginReset() + { + if (_nativeDeviceResetterProxy == null) + throw new InvalidOperationException("The native device resetter is not initialized"); + + return _nativeDeviceResetterProxy.BeginReset(); + } private event EventHandler _logEmitted; private event EventHandler _stateChanged; @@ -98,10 +102,12 @@ public async Task ResetAsync(int timeoutInMs = -1) try { - StateChanged += ResetAsyncOnStateChanged; - FatalErrorOccurred += ResetAsyncOnFatalErrorOccurred; + StateChanged += DeviceResetter_StateChanged_; + FatalErrorOccurred += DeviceResetter_FatalErrorOccurred_; - BeginReset(); //00 dont use task.run here for now + var verdict = BeginReset(); //00 dont use task.run here for now + if (verdict != EDeviceResetterInitializationVerdict.Success) + throw new ArgumentException(verdict.ToString()); _ = timeoutInMs <= 0 ? await taskCompletionSource.Task @@ -133,30 +139,27 @@ ex is not ArgumentException //10 wops probably missing native lib symbols! } finally { - StateChanged -= ResetAsyncOnStateChanged; - FatalErrorOccurred -= ResetAsyncOnFatalErrorOccurred; + StateChanged -= DeviceResetter_StateChanged_; + FatalErrorOccurred -= DeviceResetter_FatalErrorOccurred_; } return; - void ResetAsyncOnStateChanged(object sender, StateChangedEventArgs ea) + void DeviceResetter_StateChanged_(object _, StateChangedEventArgs ea_) { - if (ea.NewState != EDeviceResetterState.Complete) + if (ea_.NewState != EDeviceResetterState.Complete) return; taskCompletionSource.TrySetResult(true); } - void ResetAsyncOnFatalErrorOccurred(object sender, FatalErrorOccurredEventArgs ea) + void DeviceResetter_FatalErrorOccurred_(object _, FatalErrorOccurredEventArgs ea_) { - var isAboutUnauthorized = ea.ErrorMessage?.ToUpperInvariant().Contains("UNRECOGNIZED (11)") ?? false; - if (isAboutUnauthorized) + taskCompletionSource.TrySetException(ea_.GlobalErrorCode switch { - taskCompletionSource.TrySetException(new UnauthorizedException(ea.ErrorMessage)); - return; - } - - taskCompletionSource.TrySetException(new DeviceResetterErroredOutException(ea.ErrorMessage)); //generic + EGlobalErrorCode.McuMgrErrorBeforeSmpV2_AccessDenied => new UnauthorizedException(ea_.ErrorMessage, ea_.GlobalErrorCode), + _ => new DeviceResetterErroredOutException(ea_.ErrorMessage, ea_.GlobalErrorCode) + }); } //00 we are aware that in order to be 100% accurate about timeouts we should use task.run() here without await and then await the diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Enums/EFileDownloaderVerdict.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Enums/EFileDownloaderVerdict.cs index fd7bfcfe..67406846 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Enums/EFileDownloaderVerdict.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Enums/EFileDownloaderVerdict.cs @@ -7,6 +7,7 @@ public enum EFileDownloaderVerdict //this must mirror the java enum values of E[ { Success = 0, FailedInvalidSettings = 1, - FailedDownloadAlreadyInProgress = 2, + FailedErrorUponCommencing = 2, + FailedDownloadAlreadyInProgress = 3, } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Events/FatalErrorOccurredEventArgs.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Events/FatalErrorOccurredEventArgs.cs index a3932b55..ee837d74 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Events/FatalErrorOccurredEventArgs.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Events/FatalErrorOccurredEventArgs.cs @@ -1,7 +1,9 @@ // ReSharper disable MemberCanBePrivate.Global // ReSharper disable ClassNeverInstantiated.Global +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Events; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; namespace Laerdal.McuMgr.FileDownloader.Contracts.Events { @@ -9,11 +11,13 @@ namespace Laerdal.McuMgr.FileDownloader.Contracts.Events { public string Resource { get; } public string ErrorMessage { get; } - - public FatalErrorOccurredEventArgs(string resource, string errorMessage) + public EGlobalErrorCode GlobalErrorCode { get; } + + public FatalErrorOccurredEventArgs(string resource, string errorMessage, EGlobalErrorCode globalErrorCode) { Resource = resource; ErrorMessage = errorMessage; + GlobalErrorCode = globalErrorCode; } } } diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Exceptions/DownloadErroredOutException.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Exceptions/DownloadErroredOutException.cs index 824d1433..fcab20ac 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Exceptions/DownloadErroredOutException.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Exceptions/DownloadErroredOutException.cs @@ -1,15 +1,19 @@ using System; +using Laerdal.McuMgr.Common.Enums; namespace Laerdal.McuMgr.FileDownloader.Contracts.Exceptions { public class DownloadErroredOutException : Exception, IDownloadException { - public DownloadErroredOutException(string errorMessage) : base($"An error occurred while downloading the requested resource: '{errorMessage}'") + public EGlobalErrorCode GlobalErrorCode { get; } + + public DownloadErroredOutException(string errorMessage, EGlobalErrorCode globalErrorCode = EGlobalErrorCode.Unset) : base($"An error occurred while downloading the requested resource: '{errorMessage}'") { + GlobalErrorCode = globalErrorCode; } - + public DownloadErroredOutException(string errorMessage, Exception innerException) : base($"An error occurred while downloading the requested resource: '{errorMessage}'", innerException) { } } -} \ No newline at end of file +} diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Exceptions/DownloadErroredOutRemotePathPointsToDirectoryException.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Exceptions/DownloadErroredOutRemotePathPointsToDirectoryException.cs new file mode 100644 index 00000000..742bc6d6 --- /dev/null +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Exceptions/DownloadErroredOutRemotePathPointsToDirectoryException.cs @@ -0,0 +1,9 @@ +namespace Laerdal.McuMgr.FileDownloader.Contracts.Exceptions +{ + public sealed class DownloadErroredOutRemotePathPointsToDirectoryException : DownloadErroredOutException, IDownloadException + { + public DownloadErroredOutRemotePathPointsToDirectoryException(string remoteFilePath) : base($"The given file-path '{remoteFilePath}' points to a directory") + { + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/IFileDownloaderCommandable.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/IFileDownloaderCommandable.cs index 9d4e1162..47df4628 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/IFileDownloaderCommandable.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/IFileDownloaderCommandable.cs @@ -11,39 +11,94 @@ public interface IFileDownloaderCommandable /// Begins the file-downloading process on multiple files. Files that cannot be downloaded due to errors will have a null entry in the returned dictionary. To really know when the upgrade process has been completed you have to register to the events emitted by the downloader. /// /// The remote files to download. + /// + /// /// The amount of time to wait for each download to complete before skipping it. /// The maximum amount of tries per download before skipping and moving over to the next download. /// The amount of time to sleep between retries. + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. + /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading + /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification + /// confirming each packet. This value should be lower than or equal to MCUMGR_BUF_COUNT + /// (https://github.com/zephyrproject-rtos/zephyr/blob/bd4ddec0c8c822bbdd420bd558b62c1d1a532c16/subsys/mgmt/mcumgr/Kconfig#L550) + /// parameter in KConfig in NCS / Zephyr configuration and should also be supported on Mynewt devices. Mind, that in Zephyr, + /// before https://github.com/zephyrproject-rtos/zephyr/pull/41959 was merged, the device required data to be sent with memory alignment. + /// Otherwise, the device would ignore uneven bytes and reply with lower than expected offset + /// causing multiple packets to be sent again dropping the speed instead of increasing it. + /// (Android only) Set the selected memory alignment. Defaults to 4 to match Nordic devices. /// A dictionary containing the bytes of each remote file that got fetched over. Task> DownloadAsync( IEnumerable remoteFilePaths, + string hostDeviceModel, + string hostDeviceManufacturer, int timeoutPerDownloadInMs = -1, int maxRetriesPerDownload = 10, - int sleepTimeBetweenRetriesInMs = 0 + int sleepTimeBetweenRetriesInMs = 0, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null ); /// /// Begins the file-downloading process. To really know when the upgrade process has been completed you have to register to the events emitted by the downloader. /// /// The remote file to download. + /// + /// /// The amount of time to wait for the operation to complete before bailing out. /// The maximum amount of tries before bailing out with . /// The amount of time to sleep between retries. /// The time to wait (in milliseconds) for a cancellation request to be properly handled. If this timeout expires then the mechanism will bail out forcefully without waiting for the underlying native code to cleanup properly. + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. + /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading + /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification + /// confirming each packet. This value should be lower than or equal to MCUMGR_BUF_COUNT + /// (https://github.com/zephyrproject-rtos/zephyr/blob/bd4ddec0c8c822bbdd420bd558b62c1d1a532c16/subsys/mgmt/mcumgr/Kconfig#L550) + /// parameter in KConfig in NCS / Zephyr configuration and should also be supported on Mynewt devices. Mind, that in Zephyr, + /// before https://github.com/zephyrproject-rtos/zephyr/pull/41959 was merged, the device required data to be sent with memory alignment. + /// Otherwise, the device would ignore uneven bytes and reply with lower than expected offset + /// causing multiple packets to be sent again dropping the speed instead of increasing it. /// The bytes of the remote file that got fetched over. Task DownloadAsync( string remoteFilePath, + string hostDeviceModel, + string hostDeviceManufacturer, int timeoutForDownloadInMs = -1, int maxTriesCount = 10, int sleepTimeBetweenRetriesInMs = 1_000, - int gracefulCancellationTimeoutInMs = 2_500 + int gracefulCancellationTimeoutInMs = 2_500, + int? initialMtuSize = null, + int? windowCapacity = null ); - + /// /// Begins the file-downloading process. To really know when the upgrade process has been completed you have to register to the events emitted by the downloader. /// /// The remote file to download. - EFileDownloaderVerdict BeginDownload(string remoteFilePath); + /// + /// + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. + /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading + /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification + /// confirming each packet. This value should be lower than or equal to MCUMGR_BUF_COUNT + /// (https://github.com/zephyrproject-rtos/zephyr/blob/bd4ddec0c8c822bbdd420bd558b62c1d1a532c16/subsys/mgmt/mcumgr/Kconfig#L550) + /// parameter in KConfig in NCS / Zephyr configuration and should also be supported on Mynewt devices. Mind, that in Zephyr, + /// before https://github.com/zephyrproject-rtos/zephyr/pull/41959 was merged, the device required data to be sent with memory alignment. + /// Otherwise, the device would ignore uneven bytes and reply with lower than expected offset + /// causing multiple packets to be sent again dropping the speed instead of increasing it. + EFileDownloaderVerdict BeginDownload( + string remoteFilePath, + string hostDeviceModel, + string hostDeviceManufacturer, + int? initialMtuSize = null, + int? windowCapacity = null + ); /// Cancels the file-downloading process void Cancel(); diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCallbacksProxy.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCallbacksProxy.cs index db7156e6..5dbf04fc 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCallbacksProxy.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCallbacksProxy.cs @@ -1,5 +1,6 @@ using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; namespace Laerdal.McuMgr.FileDownloader.Contracts.Native { @@ -12,7 +13,7 @@ internal interface INativeFileDownloaderCallbacksProxy void StateChangedAdvertisement(string resource, EFileDownloaderState oldState, EFileDownloaderState newState); void BusyStateChangedAdvertisement(bool busyNotIdle); void DownloadCompletedAdvertisement(string resource, byte[] data); - void FatalErrorOccurredAdvertisement(string resource, string errorMessage); + void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EGlobalErrorCode globalErrorCode); void FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput); } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCommandableProxy.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCommandableProxy.cs index c2f89e0c..73e29c4a 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCommandableProxy.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderCommandableProxy.cs @@ -6,6 +6,10 @@ internal interface INativeFileDownloaderCommandableProxy { void Cancel(); void Disconnect(); - EFileDownloaderVerdict BeginDownload(string remoteFilePath); + EFileDownloaderVerdict BeginDownload(string remoteFilePath, int? initialMtuSize = null); + + bool TrySetContext(object context); + bool TrySetBluetoothDevice(object bluetoothDevice); + bool TryInvalidateCachedTransport(); } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderProxy.cs b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderProxy.cs index 6d18d82c..57a83dfc 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderProxy.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/Contracts/Native/INativeFileDownloaderProxy.cs @@ -3,9 +3,9 @@ namespace Laerdal.McuMgr.FileDownloader.Contracts.Native { internal interface INativeFileDownloaderProxy : + INativeFileDownloaderCallbacksProxy, INativeFileDownloaderQueryableProxy, INativeFileDownloaderCommandableProxy, - INativeFileDownloaderCallbacksProxy, IDisposable { } diff --git a/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs b/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs index 1961a75d..737be86a 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Laerdal.McuMgr.Common.Constants; using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Events; using Laerdal.McuMgr.Common.Exceptions; @@ -14,6 +15,7 @@ using Laerdal.McuMgr.FileDownloader.Contracts.Events; using Laerdal.McuMgr.FileDownloader.Contracts.Exceptions; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; namespace Laerdal.McuMgr.FileDownloader { @@ -38,12 +40,42 @@ public void Dispose() public string LastFatalErrorMessage => _nativeFileDownloaderProxy?.LastFatalErrorMessage; - public EFileDownloaderVerdict BeginDownload(string remoteFilePath) + public EFileDownloaderVerdict BeginDownload( + string remoteFilePath, + string hostDeviceModel, + string hostDeviceManufacturer, + int? initialMtuSize = null, + int? windowCapacity = null //not applicable currently but nordic considers these for future use + ) { RemoteFilePathHelpers.ValidateRemoteFilePath(remoteFilePath); // order remoteFilePath = RemoteFilePathHelpers.SanitizeRemoteFilePath(remoteFilePath); // order - var verdict = _nativeFileDownloaderProxy.BeginDownload(remoteFilePath: remoteFilePath); + var failsafeConnectionSettings = ConnectionSettingsHelpers.GetFailSafeConnectionSettingsIfHostDeviceIsProblematic( + initialMtuSize: initialMtuSize, + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + uploadingNotDownloading: false + ); + if (failsafeConnectionSettings != null) + { + initialMtuSize = failsafeConnectionSettings.Value.initialMtuSize; + // windowCapacity = connectionSettings.Value.windowCapacity; + // memoryAlignment = connectionSettings.Value.memoryAlignment; + + OnLogEmitted(new LogEmittedEventArgs( + level: ELogLevel.Warning, + message: $"[FD.BD.010] Host device '{hostDeviceModel} (made by {hostDeviceManufacturer})' is known to be problematic. Resorting to using failsafe settings " + + $"(initialMtuSize={initialMtuSize})", + resource: "File", + category: "FileDownloader" + )); + } + + var verdict = _nativeFileDownloaderProxy.BeginDownload( + remoteFilePath: remoteFilePath, + initialMtuSize: initialMtuSize + ); return verdict; } @@ -131,9 +163,14 @@ public event EventHandler> DownloadAsync( IEnumerable remoteFilePaths, + string hostDeviceModel, + string hostDeviceManufacturer, int timeoutPerDownloadInMs = -1, int maxRetriesPerDownload = 10, - int sleepTimeBetweenRetriesInMs = 0 + int sleepTimeBetweenRetriesInMs = 0, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null ) { RemoteFilePathHelpers.ValidateRemoteFilePaths(remoteFilePaths); // order @@ -150,9 +187,16 @@ public async Task> DownloadAsync( { var data = await DownloadAsync( remoteFilePath: path, - timeoutForDownloadInMs: timeoutPerDownloadInMs, + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + maxTriesCount: maxRetriesPerDownload, - sleepTimeBetweenRetriesInMs: sleepTimeBetweenRetriesInMs); + timeoutForDownloadInMs: timeoutPerDownloadInMs, + sleepTimeBetweenRetriesInMs: sleepTimeBetweenRetriesInMs, + + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity + ); results[path] = data; } @@ -172,10 +216,14 @@ public async Task> DownloadAsync( private const int DefaultGracefulCancellationTimeoutInMs = 2_500; public async Task DownloadAsync( string remoteFilePath, + string hostDeviceModel, + string hostDeviceManufacturer, int timeoutForDownloadInMs = -1, int maxTriesCount = 10, int sleepTimeBetweenRetriesInMs = 1_000, - int gracefulCancellationTimeoutInMs = DefaultGracefulCancellationTimeoutInMs + int gracefulCancellationTimeoutInMs = 2_500, + int? initialMtuSize = null, + int? windowCapacity = null ) { if (maxTriesCount <= 0) @@ -187,6 +235,9 @@ public async Task DownloadAsync( var result = (byte[])null; var isCancellationRequested = false; + var fileDownloadProgressEventsCount = 0; + var suspiciousTransportFailuresCount = 0; + var didWarnOnceAboutUnstableConnection = false; for (var triesCount = 1; !isCancellationRequested;) { var taskCompletionSource = new TaskCompletionSource(state: null); @@ -197,8 +248,40 @@ public async Task DownloadAsync( StateChanged += FileDownloader_StateChanged_; DownloadCompleted += FileDownloader_DownloadCompleted_; FatalErrorOccurred += FileDownloader_FatalErrorOccurred_; + FileDownloadProgressPercentageAndDataThroughputChanged += FileDownloader_FileDownloadProgressPercentageAndDataThroughputChanged_; + + var failSafeSettingsToApply = ConnectionSettingsHelpers.GetFailsafeConnectionSettingsIfConnectionProvedToBeUnstable( + uploadingNotDownloading: false, + triesCount: triesCount, + maxTriesCount: maxTriesCount, + suspiciousTransportFailuresCount: suspiciousTransportFailuresCount + ); + if (failSafeSettingsToApply != null) + { + initialMtuSize = failSafeSettingsToApply.Value.initialMtuSize; + windowCapacity = failSafeSettingsToApply.Value.windowCapacity; + + if (!didWarnOnceAboutUnstableConnection) + { + didWarnOnceAboutUnstableConnection = true; + OnLogEmitted(new LogEmittedEventArgs( + level: ELogLevel.Warning, + message: $"[FD.DA.010] Attempt#{triesCount}: Connection is too unstable for downloading assets from the target device. Subsequent tries will use failsafe parameters on the connection " + + $"just in case it helps (initialMtuSize={initialMtuSize?.ToString() ?? "null"}, windowCapacity={windowCapacity?.ToString() ?? "null"})", + resource: "File", + category: "FileDownloader" + )); + } + } - var verdict = BeginDownload(remoteFilePath); //00 dont use task.run here for now + var verdict = BeginDownload( //00 dont use task.run here for now + remoteFilePath: remoteFilePath, + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity + ); if (verdict != EFileDownloaderVerdict.Success) throw new ArgumentException(verdict.ToString()); @@ -222,7 +305,7 @@ public async Task DownloadAsync( } catch (DownloadErroredOutException ex) { - if (ex is DownloadErroredOutRemoteFileNotFoundException) //order no point to retry if the remote file is not there + if (ex is DownloadErroredOutRemoteFileNotFoundException or DownloadErroredOutRemotePathPointsToDirectoryException) //order no point to retry if the filepath is problematic { //OnStateChanged(new StateChangedEventArgs(newState: EFileDownloaderState.Error)); //noneed already done in native code throw; @@ -233,6 +316,11 @@ public async Task DownloadAsync( //OnStateChanged(new StateChangedEventArgs(newState: EFileDownloaderState.Error)); //noneed already done in native code throw new AllDownloadAttemptsFailedException(remoteFilePath, maxTriesCount, innerException: ex); } + + if (fileDownloadProgressEventsCount <= 10) + { + suspiciousTransportFailuresCount++; + } if (sleepTimeBetweenRetriesInMs > 0) //order { @@ -263,6 +351,7 @@ ex is not ArgumentException //10 wops probably missing native lib symbols! StateChanged -= FileDownloader_StateChanged_; DownloadCompleted -= FileDownloader_DownloadCompleted_; FatalErrorOccurred -= FileDownloader_FatalErrorOccurred_; + FileDownloadProgressPercentageAndDataThroughputChanged -= FileDownloader_FileDownloadProgressPercentageAndDataThroughputChanged_; } void FileDownloader_Cancelled_(object sender_, CancelledEventArgs ea_) @@ -272,62 +361,60 @@ void FileDownloader_Cancelled_(object sender_, CancelledEventArgs ea_) void FileDownloader_StateChanged_(object sender_, StateChangedEventArgs ea_) { - if (ea_.NewState != EFileDownloaderState.Cancelling || isCancellationRequested) - return; - - isCancellationRequested = true; - - Task.Run(async () => + switch (ea_.NewState) { - try - { - if (gracefulCancellationTimeoutInMs > 0) //keep this check here to avoid unnecessary task rescheduling + case EFileDownloaderState.Idle: + fileDownloadProgressEventsCount = 0; + return; + + case EFileDownloaderState.Cancelling: + if (isCancellationRequested) + return; + + isCancellationRequested = true; + Task.Run(async () => { - await Task.Delay(gracefulCancellationTimeoutInMs); - } - - OnCancelled(new CancelledEventArgs()); //00 - } - catch // (Exception ex) - { - // ignored - } - }); - - return; + try + { + if (gracefulCancellationTimeoutInMs > 0) //keep this check here to avoid unnecessary context switching + { + await Task.Delay(gracefulCancellationTimeoutInMs); + } + + OnCancelled(new CancelledEventArgs()); //00 + } + catch // (Exception ex) + { + // ignored + } + }); + + return; + } //00 we first wait to allow the cancellation to be handled by the underlying native code meaning that we should see OnCancelled() // getting called right above but if that takes too long we give the killing blow by calling OnCancelled() manually here } - void FileDownloader_DownloadCompleted_(object sender_, DownloadCompletedEventArgs ea_) + void FileDownloader_FileDownloadProgressPercentageAndDataThroughputChanged_(object _, FileDownloadProgressPercentageAndDataThroughputChangedEventArgs __) + { + fileDownloadProgressEventsCount++; + } + + void FileDownloader_DownloadCompleted_(object _, DownloadCompletedEventArgs ea_) { taskCompletionSource.TrySetResult(ea_.Data); } - void FileDownloader_FatalErrorOccurred_(object sender_, FatalErrorOccurredEventArgs ea_) + void FileDownloader_FatalErrorOccurred_(object _, FatalErrorOccurredEventArgs ea_) { - var isAboutUnauthorized = ea_.ErrorMessage?.ToUpperInvariant().Contains("UNRECOGNIZED (11)") ?? false; - if (isAboutUnauthorized) + taskCompletionSource.TrySetException(ea_.GlobalErrorCode switch { - taskCompletionSource.TrySetException(new UnauthorizedException( - resource: remoteFilePath, - nativeErrorMessage: ea_.ErrorMessage - )); - return; - } - - var isAboutRemoteFileNotFound = ea_.ErrorMessage - ?.ToUpperInvariant() - .Replace("NO_ENTRY (5)", "NO ENTRY (5)") //normalize the error for android so that it will be the same as in ios - .Contains("NO ENTRY (5)") ?? false; - if (isAboutRemoteFileNotFound) - { - taskCompletionSource.TrySetException(new DownloadErroredOutRemoteFileNotFoundException(remoteFilePath)); //specific case - return; - } - - taskCompletionSource.TrySetException(new DownloadErroredOutException(ea_.ErrorMessage)); //generic + EGlobalErrorCode.SubSystemFilesystem_NotFound => new DownloadErroredOutRemoteFileNotFoundException(remoteFilePath), // remote file not found + EGlobalErrorCode.SubSystemFilesystem_IsDirectory => new DownloadErroredOutRemotePathPointsToDirectoryException(remoteFilePath), // remote filepath points to a directory + EGlobalErrorCode.McuMgrErrorBeforeSmpV2_AccessDenied => new UnauthorizedException(remoteFilePath, ea_.ErrorMessage), // unauthorized + _ => new DownloadErroredOutException(remoteFilePath, ea_.GlobalErrorCode) + }); } } @@ -335,7 +422,7 @@ void FileDownloader_FatalErrorOccurred_(object sender_, FatalErrorOccurredEventA throw new DownloadCancelledException(); //20 return result; - + //00 we are aware that in order to be 100% accurate about timeouts we should use task.run() here without await and then await the // taskcompletionsource right after but if we went down this path we would also have to account for exceptions thus complicating // the code considerably for little to no practical gain considering that the native call has trivial setup code and is very fast @@ -359,10 +446,11 @@ void FileDownloader_FatalErrorOccurred_(object sender_, FatalErrorOccurredEventA private void OnCancelled(CancelledEventArgs ea) => _cancelled?.Invoke(this, ea); private void OnLogEmitted(LogEmittedEventArgs ea) => _logEmitted?.Invoke(this, ea); - private void OnStateChanged(StateChangedEventArgs ea) => _stateChanged?.Invoke(this, ea); private void OnBusyStateChanged(BusyStateChangedEventArgs ea) => _busyStateChanged?.Invoke(this, ea); private void OnDownloadCompleted(DownloadCompletedEventArgs ea) => _downloadCompleted?.Invoke(this, ea); private void OnFatalErrorOccurred(FatalErrorOccurredEventArgs ea) => _fatalErrorOccurred?.Invoke(this, ea); + + private void OnStateChanged(StateChangedEventArgs ea) => _stateChanged?.Invoke(this, ea); private void OnFileDownloadProgressPercentageAndDataThroughputChanged(FileDownloadProgressPercentageAndDataThroughputChangedEventArgs ea) => _fileDownloadProgressPercentageAndDataThroughputChanged?.Invoke(this, ea); //this sort of approach proved to be necessary for our testsuite to be able to effectively mock away the INativeFileDownloaderProxy @@ -394,8 +482,8 @@ public void BusyStateChangedAdvertisement(bool busyNotIdle) public void DownloadCompletedAdvertisement(string resource, byte[] data) => FileDownloader?.OnDownloadCompleted(new DownloadCompletedEventArgs(resource, data)); - public void FatalErrorOccurredAdvertisement(string resource, string errorMessage) - => FileDownloader?.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(resource, errorMessage)); + public void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EGlobalErrorCode globalErrorCode) + => FileDownloader?.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(resource, errorMessage, globalErrorCode)); public void FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput) => FileDownloader?.OnFileDownloadProgressPercentageAndDataThroughputChanged(new FileDownloadProgressPercentageAndDataThroughputChangedEventArgs( diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Enums/EFileUploaderGroupReturnCode.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Enums/EFileUploaderGroupReturnCode.cs deleted file mode 100644 index a5da93c9..00000000 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Enums/EFileUploaderGroupReturnCode.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Laerdal.McuMgr.FileUploader.Contracts.Enums -{ - public enum EFileUploaderGroupReturnCode //@formatter:off - { - Unset = -99, - Ok = 00, - Unknown = 01, - InvalidName = 02, - NotFound = 03, - IsDirectory = 04, - OpenFailed = 05, - SeekFailed = 06, - ReadFailed = 07, - TruncateFailed = 08, - DeleteFailed = 09, - WriteFailed = 10, - OffsetNotValid = 11, - OffsetLargerThanFile = 12, - ChecksumHashNotFound = 13, - MountPointNotFound = 14, - ReadOnlyFilesystem = 15, - FileEmpty = 16, //@formatter:on - } -} diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Enums/EFileUploaderVerdict.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Enums/EFileUploaderVerdict.cs index 72523071..0456f23c 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Enums/EFileUploaderVerdict.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Enums/EFileUploaderVerdict.cs @@ -6,8 +6,9 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Enums public enum EFileUploaderVerdict //this must mirror the java enum values of E[Android|iOS]FileUploaderVerdict { Success = 0, - FailedInvalidSettings = 1, - FailedInvalidData = 2, - FailedOtherUploadAlreadyInProgress = 3, + FailedInvalidData = 1, + FailedInvalidSettings = 2, + FailedErrorUponCommencing = 3, + FailedOtherUploadAlreadyInProgress = 4, } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/CancelledEventArgs.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/CancelledEventArgs.cs index 5a781ea0..f2a0ea58 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/CancelledEventArgs.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/CancelledEventArgs.cs @@ -7,5 +7,11 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Events { public readonly struct CancelledEventArgs : IMcuMgrEventArgs { + public string Reason { get; } + + public CancelledEventArgs(string reason) + { + Reason = reason; + } } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/CancellingEventArgs.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/CancellingEventArgs.cs new file mode 100644 index 00000000..07727532 --- /dev/null +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/CancellingEventArgs.cs @@ -0,0 +1,14 @@ +using Laerdal.McuMgr.Common.Events; + +namespace Laerdal.McuMgr.FileUploader.Contracts.Events +{ + public readonly struct CancellingEventArgs : IMcuMgrEventArgs + { + public string Reason { get; } + + public CancellingEventArgs(string reason) + { + Reason = reason; + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/FatalErrorOccurredEventArgs.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/FatalErrorOccurredEventArgs.cs index 3ce54b59..0f7c5da9 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/FatalErrorOccurredEventArgs.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Events/FatalErrorOccurredEventArgs.cs @@ -12,15 +12,13 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Events public string ErrorMessage { get; } public string RemoteFilePath { get; } - public EMcuMgrErrorCode ErrorCode { get; } - public EFileUploaderGroupReturnCode GroupReturnCode { get; } + public EGlobalErrorCode GlobalErrorCode { get; } - public FatalErrorOccurredEventArgs(string remoteFilePath, string errorMessage, EMcuMgrErrorCode errorCode, EFileUploaderGroupReturnCode groupReturnCode) + public FatalErrorOccurredEventArgs(string remoteFilePath, string errorMessage, EGlobalErrorCode globalErrorCode) { - ErrorCode = errorCode; ErrorMessage = errorMessage; RemoteFilePath = remoteFilePath; - GroupReturnCode = groupReturnCode; + GlobalErrorCode = globalErrorCode; } } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/AllUploadAttemptsFailedException.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/AllUploadAttemptsFailedException.cs index 897d4e7b..94ed3e64 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/AllUploadAttemptsFailedException.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/AllUploadAttemptsFailedException.cs @@ -5,7 +5,7 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Exceptions public class AllUploadAttemptsFailedException : UploadErroredOutException, IUploadException { public AllUploadAttemptsFailedException(string remoteFilePath, int triesCount, Exception innerException = null) - : base(remoteFilePath, $"Failed to upload '{remoteFilePath}' after trying {triesCount} time(s)", innerException) + : base(remoteFilePath, $"Failed to upload '{remoteFilePath}' after trying {triesCount} time(s)", innerException: innerException) { } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadCancelledException.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadCancelledException.cs index b38e85c6..f2dbf302 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadCancelledException.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadCancelledException.cs @@ -4,7 +4,8 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Exceptions { public class UploadCancelledException : Exception, IUploadException { - public UploadCancelledException() : base("Upload was cancelled") + public UploadCancelledException(string optionalReason = "") + : base($"Upload was cancelled{(string.IsNullOrWhiteSpace(optionalReason) ? "" : $": {optionalReason}")}") { } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutException.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutException.cs index 6bfb2e4c..ef9c38a9 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutException.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutException.cs @@ -7,27 +7,17 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Exceptions public class UploadErroredOutException : Exception, IUploadException { public string RemoteFilePath { get; } - - public EMcuMgrErrorCode McuMgrErrorCode { get; } = EMcuMgrErrorCode.Unset; - public EFileUploaderGroupReturnCode GroupReturnCode { get; } = EFileUploaderGroupReturnCode.Unset; - - protected UploadErroredOutException(string remoteFilePath, string errorMessage, Exception innerException = null) - : base($"An error occurred while uploading over to '{remoteFilePath}': '{errorMessage}'", innerException) - { - RemoteFilePath = remoteFilePath; - } + public EGlobalErrorCode GlobalErrorCode { get; } public UploadErroredOutException( string nativeErrorMessage, string remoteFilePath, - EMcuMgrErrorCode mcuMgrErrorCode, - EFileUploaderGroupReturnCode groupReturnCode, + EGlobalErrorCode globalErrorCode = EGlobalErrorCode.Unset, Exception innerException = null - ) : base($"An error occurred while uploading '{remoteFilePath}': '{nativeErrorMessage}' (mcuMgrErrorCode={mcuMgrErrorCode}, groupReturnCode={groupReturnCode})", innerException) + ) : base($"An error occurred while uploading '{remoteFilePath}': '{nativeErrorMessage}' (globalErrorCode={globalErrorCode})", innerException) { RemoteFilePath = remoteFilePath; - McuMgrErrorCode = mcuMgrErrorCode; - GroupReturnCode = groupReturnCode; + GlobalErrorCode = globalErrorCode; } } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutRemoteFolderNotFoundException.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutRemoteFolderNotFoundException.cs index 5e81cfcf..ded82ec7 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutRemoteFolderNotFoundException.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadErroredOutRemoteFolderNotFoundException.cs @@ -10,13 +10,11 @@ public sealed class UploadErroredOutRemoteFolderNotFoundException : UploadErrore public UploadErroredOutRemoteFolderNotFoundException( string nativeErrorMessage, string remoteFilePath, - EMcuMgrErrorCode mcuMgrErrorCode, - EFileUploaderGroupReturnCode groupReturnCode + EGlobalErrorCode globalErrorCode ) : base( - nativeErrorMessage: nativeErrorMessage, remoteFilePath: remoteFilePath, - mcuMgrErrorCode: mcuMgrErrorCode, - groupReturnCode: groupReturnCode + globalErrorCode: globalErrorCode, + nativeErrorMessage: nativeErrorMessage ) { } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadInternalErrorException.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadInternalErrorException.cs index 8c632721..a315db5e 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadInternalErrorException.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadInternalErrorException.cs @@ -5,7 +5,7 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Exceptions public class UploadInternalErrorException : UploadErroredOutException, IUploadException { public UploadInternalErrorException(string remoteFilePath, Exception innerException = null) - : base(remoteFilePath, "An internal error occured - report what you did to reproduce this because this is most probably a bug!", innerException) + : base(remoteFilePath, "An internal error occured - report what you did to reproduce this because this is most probably a bug!", innerException: innerException) { } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadTimeoutException.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadTimeoutException.cs index d7eb254c..fbba50d1 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadTimeoutException.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadTimeoutException.cs @@ -7,7 +7,7 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Exceptions public sealed class UploadTimeoutException : UploadErroredOutException, IUploadException { public UploadTimeoutException(string remoteFilePath, int timeoutInMs, Exception innerException) - : base(remoteFilePath, $"Failed to upload over to '{remoteFilePath}' on the device within {timeoutInMs}ms", innerException) + : base(remoteFilePath, $"Failed to upload over to '{remoteFilePath}' on the device within {timeoutInMs}ms", innerException: innerException) { } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadUnauthorizedException.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadUnauthorizedException.cs index 3d789cf0..9a1a6465 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadUnauthorizedException.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Exceptions/UploadUnauthorizedException.cs @@ -7,16 +7,11 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Exceptions public class UploadUnauthorizedException : UploadErroredOutException, IMcuMgrException { public string RemoteFilePath { get; } - - public EMcuMgrErrorCode McuMgrErrorCode { get; } - public EFileUploaderGroupReturnCode GroupReturnCode { get; } - public UploadUnauthorizedException(string nativeErrorMessage, string remoteFilePath, EMcuMgrErrorCode mcuMgrErrorCode, EFileUploaderGroupReturnCode groupReturnCode) - : base(remoteFilePath, $"{nativeErrorMessage} (McuMgrErrorCode={mcuMgrErrorCode}, GroupReturnCode={groupReturnCode})") + public UploadUnauthorizedException(string nativeErrorMessage, string remoteFilePath, EGlobalErrorCode globalErrorCode) + : base(remoteFilePath, $"{nativeErrorMessage}", globalErrorCode) { RemoteFilePath = remoteFilePath; - McuMgrErrorCode = mcuMgrErrorCode; - GroupReturnCode = groupReturnCode; } } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploader.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploader.cs index f416653e..f28264d8 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploader.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploader.cs @@ -8,11 +8,12 @@ namespace Laerdal.McuMgr.FileUploader.Contracts /// Uploads a file on a specific Nordic-chip-based BLE device /// For the file-uploading process to even commence you need to be authenticated with the AED device that's being targeted. public interface IFileUploader : - IFileUploaderCommandable, IFileUploaderQueryable, - IFileUploaderEventSubscribable, + IFileUploaderCommandable, IFileUploaderCleanupable, + IFileUploaderEventSubscribable, IDisposable + //IFileUploaderEventEmittable dont this interface is meant to be internal only { } } diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderCommandable.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderCommandable.cs index 71853990..3df57cef 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderCommandable.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderCommandable.cs @@ -18,18 +18,43 @@ public interface IFileUploaderCommandable /// Allowed types for TData are 'Stream', 'Func<Stream>', 'Func<Task<Stream>>', 'Func<ValueTask<Stream>>', 'byte[]' and 'IEnumerable<byte>'. /// /// The files to upload. + /// The manufacturer of the host-device + /// The device-model of the host-device /// The time to sleep between each retry after a failed try. /// The amount of time to wait for each upload to complete before bailing out. /// Maximum amount of tries per upload before bailing out. In case of errors the mechanism will try "maxTriesPerUpload" before bailing out. /// If set to 'true' (which is the default) the mechanism will move to the next file to upload whenever a particular file fails to be uploaded despite all retries /// If set to 'true' the mechanism will dispose of the data-streams after they have been read into their respective byte arrays (default is 'false'). + /// (iOS only) If set to a value larger than 1, this enables SMP Pipelining, wherein multiple packets of data ('chunks') are sent at + /// once before awaiting a response, which can lead to a big increase in transfer speed if the receiving hardware supports this feature. + /// (iOS only) When PipelineLength is larger than 1 (SMP Pipelining Enabled) it's necessary to set this in order for the stack + /// to predict offset jumps as multiple packets are sent in parallel. + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. + /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading + /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification + /// confirming each packet. This value should be lower than or equal to MCUMGR_BUF_COUNT + /// (https://github.com/zephyrproject-rtos/zephyr/blob/bd4ddec0c8c822bbdd420bd558b62c1d1a532c16/subsys/mgmt/mcumgr/Kconfig#L550) + /// parameter in KConfig in NCS / Zephyr configuration and should also be supported on Mynewt devices. Mind, that in Zephyr, + /// before https://github.com/zephyrproject-rtos/zephyr/pull/41959 was merged, the device required data to be sent with memory alignment. + /// Otherwise, the device would ignore uneven bytes and reply with lower than expected offset + /// causing multiple packets to be sent again dropping the speed instead of increasing it. + /// (Android only) Set the selected memory alignment. Defaults to 4 to match Nordic devices. Task> UploadAsync( IDictionary remoteFilePathsAndTheirData, + string hostDeviceModel, + string hostDeviceManufacturer, int sleepTimeBetweenRetriesInMs = 100, int timeoutPerUploadInMs = -1, int maxTriesPerUpload = 10, bool moveToNextUploadInCaseOfError = true, - bool autodisposeStreams = false + bool autodisposeStreams = false, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null ); /// Uploads the given data (typically representing the contents of a file either as a stream or a raw byte array). @@ -43,27 +68,80 @@ Task> UploadAsync( /// /// The local data to upload. /// The remote file-path to upload the data to. + /// The manufacturer of the host-device + /// The device-model of the host-device /// The amount of time to wait for the upload to complete before bailing out. /// The maximum amount of tries before bailing out with . /// The time to sleep between each retry after a failed try. /// The time to wait (in milliseconds) for a cancellation request to be properly handled. If this timeout expires then the mechanism will bail out forcefully without waiting for the underlying native code to cleanup properly. /// If set to 'true' the mechanism will dispose of the data-stream after it has been read into a byte array (default is 'false'). + /// (iOS only) If set to a value larger than 1, this enables SMP Pipelining, wherein multiple packets of data ('chunks') are sent at + /// once before awaiting a response, which can lead to a big increase in transfer speed if the receiving hardware supports this feature. + /// (iOS only) When PipelineLength is larger than 1 (SMP Pipelining Enabled) it's necessary to set this in order for the stack + /// to predict offset jumps as multiple packets are sent in parallel. + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. + /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading + /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification + /// confirming each packet. This value should be lower than or equal to MCUMGR_BUF_COUNT + /// (https://github.com/zephyrproject-rtos/zephyr/blob/bd4ddec0c8c822bbdd420bd558b62c1d1a532c16/subsys/mgmt/mcumgr/Kconfig#L550) + /// parameter in KConfig in NCS / Zephyr configuration and should also be supported on Mynewt devices. Mind, that in Zephyr, + /// before https://github.com/zephyrproject-rtos/zephyr/pull/41959 was merged, the device required data to be sent with memory alignment. + /// Otherwise, the device would ignore uneven bytes and reply with lower than expected offset + /// causing multiple packets to be sent again dropping the speed instead of increasing it. + /// (Android only) Set the selected memory alignment. Defaults to 4 to match Nordic devices. Task UploadAsync( TData localData, string remoteFilePath, + string hostDeviceModel, + string hostDeviceManufacturer, int timeoutForUploadInMs = -1, int maxTriesCount = 10, int sleepTimeBetweenRetriesInMs = 1_000, int gracefulCancellationTimeoutInMs = 2_500, - bool autodisposeStream = false + bool autodisposeStream = false, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null ); - + /// /// Begins the file-uploading process. To really know when the upgrade process has been completed you have to register to the events emitted by the uploader. /// /// The remote file-path to upload the data to. /// The file-data. - EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data); + /// The manufacturer of the host-device + /// The device-model of the host-device + /// (iOS only) If set to a value larger than 1, this enables SMP Pipelining, wherein multiple packets of data ('chunks') are sent at + /// once before awaiting a response, which can lead to a big increase in transfer speed if the receiving hardware supports this feature. + /// (iOS only) When PipelineLength is larger than 1 (SMP Pipelining Enabled) it's necessary to set this in order for the stack + /// to predict offset jumps as multiple packets are sent in parallel. + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. + /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading + /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification + /// confirming each packet. This value should be lower than or equal to MCUMGR_BUF_COUNT + /// (https://github.com/zephyrproject-rtos/zephyr/blob/bd4ddec0c8c822bbdd420bd558b62c1d1a532c16/subsys/mgmt/mcumgr/Kconfig#L550) + /// parameter in KConfig in NCS / Zephyr configuration and should also be supported on Mynewt devices. Mind, that in Zephyr, + /// before https://github.com/zephyrproject-rtos/zephyr/pull/41959 was merged, the device required data to be sent with memory alignment. + /// Otherwise, the device would ignore uneven bytes and reply with lower than expected offset + /// causing multiple packets to be sent again dropping the speed instead of increasing it. + /// (Android only) Set the selected memory alignment. Defaults to 4 to match Nordic devices. + EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + string hostDeviceModel, + string hostDeviceManufacturer, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null + ); /// /// Scraps the current transport. This is useful in case the transport is in a bad state and needs to be restarted. @@ -81,7 +159,8 @@ Task UploadAsync( bool TrySetBluetoothDevice(object bluetoothDevice); /// Cancels the file-uploading process - void Cancel(); + /// (optional) The reason for the cancellation + void Cancel(string reason = ""); /// Disconnects the file-uploader from the targeted device void Disconnect(); diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderEventEmittable.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderEventEmittable.cs index abc52559..db8dcb70 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderEventEmittable.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/IFileUploaderEventEmittable.cs @@ -3,9 +3,11 @@ namespace Laerdal.McuMgr.FileUploader.Contracts { + //must be internal because there is absolutely no point for anyone outside this assembly to be able to raise these events internal interface IFileUploaderEventEmittable { void OnCancelled(CancelledEventArgs ea); + void OnCancelling(CancellingEventArgs ea); void OnLogEmitted(LogEmittedEventArgs ea); void OnStateChanged(StateChangedEventArgs ea); void OnFileUploaded(FileUploadedEventArgs ea); diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCallbacksProxy.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCallbacksProxy.cs index e7c1ac5a..96e286ad 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCallbacksProxy.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCallbacksProxy.cs @@ -7,12 +7,14 @@ internal interface INativeFileUploaderCallbacksProxy { public IFileUploaderEventEmittable FileUploader { get; set; } - void CancelledAdvertisement(); + void CancelledAdvertisement(string reason = ""); + void CancellingAdvertisement(string reason = ""); + void LogMessageAdvertisement(string message, string category, ELogLevel level, string resource); void StateChangedAdvertisement(string resource, EFileUploaderState oldState, EFileUploaderState newState); void BusyStateChangedAdvertisement(bool busyNotIdle); void FileUploadedAdvertisement(string resource); - void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EMcuMgrErrorCode mcuMgrErrorCode, EFileUploaderGroupReturnCode fileUploaderGroupReturnCode); + void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EGlobalErrorCode globalErrorCode); void FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput); } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCommandableProxy.cs b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCommandableProxy.cs index 56f36c20..c9adf67e 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCommandableProxy.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/Contracts/Native/INativeFileUploaderCommandableProxy.cs @@ -4,10 +4,18 @@ namespace Laerdal.McuMgr.FileUploader.Contracts.Native { internal interface INativeFileUploaderCommandableProxy { - void Cancel(); + void Cancel(string reason = ""); void Disconnect(); - - EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data); + + EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null + ); bool TrySetContext(object context); bool TrySetBluetoothDevice(object bluetoothDevice); diff --git a/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs b/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs index fbd55b40..cf822540 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Laerdal.McuMgr.Common.Constants; using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Events; using Laerdal.McuMgr.Common.Helpers; @@ -39,26 +40,77 @@ public void Dispose() } public bool TrySetContext(object context) => _nativeFileUploaderProxy?.TrySetContext(context) ?? false; - public bool TrySetBluetoothDevice(object bluetoothDevice) => _nativeFileUploaderProxy?.TrySetContext(bluetoothDevice) ?? false; + public bool TrySetBluetoothDevice(object bluetoothDevice) => _nativeFileUploaderProxy?.TrySetBluetoothDevice(bluetoothDevice) ?? false; public bool TryInvalidateCachedTransport() => _nativeFileUploaderProxy?.TryInvalidateCachedTransport() ?? false; - public EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + public EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + + string hostDeviceModel, + string hostDeviceManufacturer, + + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null + ) { data = data ?? throw new ArgumentNullException(nameof(data)); RemoteFilePathHelpers.ValidateRemoteFilePath(remoteFilePath); // order remoteFilePath = RemoteFilePathHelpers.SanitizeRemoteFilePath(remoteFilePath); // order - var verdict = _nativeFileUploaderProxy.BeginUpload(remoteFilePath, data); + var failsafeConnectionSettings = ConnectionSettingsHelpers.GetFailSafeConnectionSettingsIfHostDeviceIsProblematic( + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + + pipelineDepth: pipelineDepth, + byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment, + uploadingNotDownloading: true + ); + if (failsafeConnectionSettings != null) + { + pipelineDepth = failsafeConnectionSettings.Value.pipelineDepth; + byteAlignment = failsafeConnectionSettings.Value.byteAlignment; + initialMtuSize = failsafeConnectionSettings.Value.initialMtuSize; + windowCapacity = failsafeConnectionSettings.Value.windowCapacity; + memoryAlignment = failsafeConnectionSettings.Value.memoryAlignment; + + OnLogEmitted(new LogEmittedEventArgs( + level: ELogLevel.Warning, + message: $"[FU.BU.010] Host device '{hostDeviceModel} (made by {hostDeviceManufacturer})' is known to be problematic. Resorting to using failsafe settings " + + $"(pipelineDepth={pipelineDepth ?.ToString() ?? "null"}, byteAlignment={byteAlignment?.ToString() ?? "null"}, initialMtuSize={initialMtuSize?.ToString() ?? "null"}, windowCapacity={windowCapacity?.ToString() ?? "null"}, memoryAlignment={memoryAlignment?.ToString() ?? "null"})", + resource: "File", + category: "FileDownloader" + )); + } + + var verdict = _nativeFileUploaderProxy.BeginUpload( + data: data, + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth, + byteAlignment: byteAlignment, + + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment + ); return verdict; } - public void Cancel() => _nativeFileUploaderProxy?.Cancel(); + public void Cancel(string reason = "") => _nativeFileUploaderProxy?.Cancel(reason); public void Disconnect() => _nativeFileUploaderProxy?.Disconnect(); public void CleanupResourcesOfLastUpload() => _nativeFileUploaderProxy?.CleanupResourcesOfLastUpload(); private event EventHandler _cancelled; + private event EventHandler _cancelling; private event EventHandler _logEmitted; private event EventHandler _stateChanged; private event EventHandler _fileUploaded; @@ -86,6 +138,16 @@ public event EventHandler LogEmitted remove => _logEmitted -= value; } + public event EventHandler Cancelling + { + add + { + _cancelling -= value; + _cancelling += value; + } + remove => _cancelling -= value; + } + public event EventHandler Cancelled { add @@ -139,11 +201,18 @@ public event EventHandler> UploadAsync( IDictionary remoteFilePathsAndTheirData, + string hostDeviceModel, + string hostDeviceManufacturer, int sleepTimeBetweenRetriesInMs = 100, int timeoutPerUploadInMs = -1, int maxTriesPerUpload = 10, bool moveToNextUploadInCaseOfError = true, - bool autodisposeStreams = false + bool autodisposeStreams = false, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null ) where TData : notnull { RemoteFilePathHelpers.ValidateRemoteFilePathsWithDataBytes(remoteFilePathsAndTheirData); @@ -158,12 +227,21 @@ public async Task> UploadAsync( await UploadAsync( data: x.Value, remoteFilePath: x.Key, + + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + timeoutForUploadInMs: timeoutPerUploadInMs, maxTriesCount: maxTriesPerUpload, + + sleepTimeBetweenRetriesInMs: sleepTimeBetweenRetriesInMs, autodisposeStream: autodisposeStreams, - timeoutForUploadInMs: timeoutPerUploadInMs, - sleepTimeBetweenRetriesInMs: sleepTimeBetweenRetriesInMs - ); + + pipelineDepth: pipelineDepth, + byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment); } catch (UploadErroredOutException) { @@ -180,7 +258,7 @@ await UploadAsync( return filesThatFailedToBeUploaded; //00 we prefer to upload as many files as possible and report any failures collectively at the very end we resorted to this - // tactic because failures are fairly common when uploading 50 files or more over to aed devices and we wanted to ensure + // tactic because failures are fairly common when uploading 50 files or more over to aed devices, and we wanted to ensure // that it would be as easy as possible to achieve the mass uploading just by using the default settings } @@ -188,11 +266,18 @@ await UploadAsync( public async Task UploadAsync( TData data, string remoteFilePath, + string hostDeviceModel, + string hostDeviceManufacturer, int timeoutForUploadInMs = -1, int maxTriesCount = 10, int sleepTimeBetweenRetriesInMs = 1_000, int gracefulCancellationTimeoutInMs = 2_500, - bool autodisposeStream = false + bool autodisposeStream = false, + int? pipelineDepth = null, + int? byteAlignment = null, + int? initialMtuSize = null, + int? windowCapacity = null, + int? memoryAlignment = null ) where TData : notnull { if (data is null) @@ -207,18 +292,63 @@ public async Task UploadAsync( ? gracefulCancellationTimeoutInMs : DefaultGracefulCancellationTimeoutInMs; + var cancellationReason = ""; var isCancellationRequested = false; + var fileUploadProgressEventsCount = 0; + var suspiciousTransportFailuresCount = 0; + var didWarnOnceAboutUnstableConnection = false; for (var triesCount = 1; !isCancellationRequested;) { var taskCompletionSource = new TaskCompletionSource(state: false); try { Cancelled += FileUploader_Cancelled_; + Cancelling += FileUploader_Cancelling_; FileUploaded += FileUploader_FileUploaded_; StateChanged += FileUploader_StateChanged_; FatalErrorOccurred += FileUploader_FatalErrorOccurred_; + FileUploadProgressPercentageAndDataThroughputChanged += FileUploader_FileUploadProgressPercentageAndDataThroughputChanged_; + + var failSafeSettingsToApply = ConnectionSettingsHelpers.GetFailsafeConnectionSettingsIfConnectionProvedToBeUnstable( + uploadingNotDownloading: true, + triesCount: triesCount, + maxTriesCount: maxTriesCount, + suspiciousTransportFailuresCount: suspiciousTransportFailuresCount + ); + if (failSafeSettingsToApply != null) + { + byteAlignment = failSafeSettingsToApply.Value.byteAlignment; + pipelineDepth = failSafeSettingsToApply.Value.pipelineDepth; + initialMtuSize = failSafeSettingsToApply.Value.initialMtuSize; + windowCapacity = failSafeSettingsToApply.Value.windowCapacity; + memoryAlignment = failSafeSettingsToApply.Value.memoryAlignment; + + if (!didWarnOnceAboutUnstableConnection) + { + didWarnOnceAboutUnstableConnection = true; + OnLogEmitted(new LogEmittedEventArgs( + level: ELogLevel.Warning, + message: $"[FU.UA.010] Attempt#{triesCount}: Connection is too unstable for uploading assets to the target device. Subsequent tries will use failsafe parameters on the connection " + + $"just in case it helps (byteAlignment={byteAlignment}, pipelineDepth={pipelineDepth}, initialMtuSize={initialMtuSize}, windowCapacity={windowCapacity}, memoryAlignment={memoryAlignment})", + resource: "File", + category: "FileUploader" + )); + } + } - var verdict = BeginUpload(remoteFilePath, dataArray); //00 dont use task.run here for now + var verdict = BeginUpload( //00 dont use task.run here for now + remoteFilePath: remoteFilePath, + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + + data: dataArray, // ios only + pipelineDepth: pipelineDepth, // ios only + + byteAlignment: byteAlignment, // android only + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment // android only + ); if (verdict != EFileUploaderVerdict.Success) throw new ArgumentException(verdict.ToString()); @@ -230,7 +360,9 @@ public async Task UploadAsync( } catch (TimeoutException ex) { - (this as IFileUploaderEventEmittable).OnStateChanged(new StateChangedEventArgs( //for consistency + //todo silently cancel the upload here on best effort basis + + OnStateChanged(new StateChangedEventArgs( //for consistency resource: remoteFilePath, oldState: EFileUploaderState.None, //better not use this.State here because the native call might fail newState: EFileUploaderState.Error @@ -246,6 +378,11 @@ public async Task UploadAsync( if (++triesCount > maxTriesCount) //order throw new AllUploadAttemptsFailedException(remoteFilePath, maxTriesCount, innerException: ex); + if (fileUploadProgressEventsCount <= 10) + { + suspiciousTransportFailuresCount++; + } + if (sleepTimeBetweenRetriesInMs > 0) //order { await Task.Delay(sleepTimeBetweenRetriesInMs); @@ -259,7 +396,7 @@ ex is not ArgumentException //10 wops probably missing native lib symbols! && !(ex is IUploadException) //this accounts for both cancellations and upload errors ) { - (this as IFileUploaderEventEmittable).OnStateChanged(new StateChangedEventArgs( //for consistency + OnStateChanged(new StateChangedEventArgs( //for consistency resource: remoteFilePath, oldState: EFileUploaderState.None, newState: EFileUploaderState.Error @@ -272,16 +409,18 @@ ex is not ArgumentException //10 wops probably missing native lib symbols! finally { Cancelled -= FileUploader_Cancelled_; + Cancelling -= FileUploader_Cancelling_; FileUploaded -= FileUploader_FileUploaded_; StateChanged -= FileUploader_StateChanged_; FatalErrorOccurred -= FileUploader_FatalErrorOccurred_; + FileUploadProgressPercentageAndDataThroughputChanged -= FileUploader_FileUploadProgressPercentageAndDataThroughputChanged_; CleanupResourcesOfLastUpload(); //vital } void FileUploader_Cancelled_(object _, CancelledEventArgs ea_) { - taskCompletionSource.TrySetException(new UploadCancelledException()); + taskCompletionSource.TrySetException(new UploadCancelledException(ea_.Reason)); } void FileUploader_FileUploaded_(object _, FileUploadedEventArgs ea_) @@ -289,85 +428,70 @@ void FileUploader_FileUploaded_(object _, FileUploadedEventArgs ea_) taskCompletionSource.TrySetResult(true); } - // ReSharper disable AccessToModifiedClosure - void FileUploader_StateChanged_(object _, StateChangedEventArgs ea_) + void FileUploader_Cancelling_(object _, CancellingEventArgs ea_) { - switch (ea_.NewState) - { - case EFileUploaderState.Complete: - //taskCompletionSource.TrySetResult(true); //dont we want to wait for the FileUploaded event - return; - - case EFileUploaderState.Cancelling: //20 - if (isCancellationRequested) - return; + if (isCancellationRequested) + return; - isCancellationRequested = true; + cancellationReason = ea_.Reason; + isCancellationRequested = true; - Task.Run(async () => + Task.Run(async () => + { + try + { + if (gracefulCancellationTimeoutInMs > 0) //keep this check here to avoid unnecessary task rescheduling { - try - { - if (gracefulCancellationTimeoutInMs > 0) //keep this check here to avoid unnecessary task rescheduling - { - await Task.Delay(gracefulCancellationTimeoutInMs); - } - - (this as IFileUploaderEventEmittable).OnCancelled(new CancelledEventArgs()); //00 - } - catch // (Exception ex) - { - // ignored - } - }); - return; - } - + await Task.Delay(gracefulCancellationTimeoutInMs); + } + + OnCancelled(new CancelledEventArgs(ea_.Reason)); //00 + } + catch // (Exception ex) + { + // ignored + } + }); + //00 we first wait to allow the cancellation to be handled by the underlying native code meaning that we should see OnCancelled() // getting called right above but if that takes too long we give the killing blow by calling OnCancelled() manually here } - void FileUploader_FatalErrorOccurred_(object sender, FatalErrorOccurredEventArgs ea) + void FileUploader_StateChanged_(object _, StateChangedEventArgs ea_) // ReSharper disable AccessToModifiedClosure { - var isAboutUnauthorized = ea.ErrorCode == EMcuMgrErrorCode.AccessDenied; - if (isAboutUnauthorized) - { - taskCompletionSource.TrySetException(new UploadUnauthorizedException( //specific case - remoteFilePath: remoteFilePath, - mcuMgrErrorCode: ea.ErrorCode, - groupReturnCode: ea.GroupReturnCode, - nativeErrorMessage: ea.ErrorMessage - )); - return; - } - - var isAboutFolderNotExisting = ea.ErrorCode == EMcuMgrErrorCode.Unknown; - if (isAboutFolderNotExisting) + switch (ea_.NewState) { - taskCompletionSource.TrySetException(new UploadErroredOutRemoteFolderNotFoundException( //specific case - remoteFilePath: remoteFilePath, - mcuMgrErrorCode: ea.ErrorCode, - groupReturnCode: ea.GroupReturnCode, - nativeErrorMessage: ea.ErrorMessage - )); - return; + case EFileUploaderState.Idle: + fileUploadProgressEventsCount = 0; //it is vital to reset the counter here to account for retries + return; + + case EFileUploaderState.Complete: + //taskCompletionSource.TrySetResult(true); //dont we want to wait for the FileUploaded event + return; } + } // ReSharper restore AccessToModifiedClosure - taskCompletionSource.TrySetException(new UploadErroredOutException( //generic - remoteFilePath: remoteFilePath, - mcuMgrErrorCode: ea.ErrorCode, - groupReturnCode: ea.GroupReturnCode, - nativeErrorMessage: ea.ErrorMessage - )); + void FileUploader_FileUploadProgressPercentageAndDataThroughputChanged_(object _, FileUploadProgressPercentageAndDataThroughputChangedEventArgs __) + { + fileUploadProgressEventsCount++; + } + + void FileUploader_FatalErrorOccurred_(object _, FatalErrorOccurredEventArgs ea_) + { + taskCompletionSource.TrySetException(ea_.GlobalErrorCode switch + { + EGlobalErrorCode.SubSystemFilesystem_NotFound => new UploadErroredOutRemoteFolderNotFoundException(remoteFilePath: remoteFilePath, nativeErrorMessage: ea_.ErrorMessage, globalErrorCode: ea_.GlobalErrorCode), + EGlobalErrorCode.McuMgrErrorBeforeSmpV2_AccessDenied => new UploadUnauthorizedException(remoteFilePath: remoteFilePath, nativeErrorMessage: ea_.ErrorMessage, globalErrorCode: ea_.GlobalErrorCode), + _ => new UploadErroredOutException(remoteFilePath: remoteFilePath, globalErrorCode: ea_.GlobalErrorCode, nativeErrorMessage: ea_.ErrorMessage) + }); } - // ReSharper restore AccessToModifiedClosure } if (isCancellationRequested) //vital - throw new UploadCancelledException(); //20 + throw new UploadCancelledException(cancellationReason); //20 return; - + //00 we are aware that in order to be 100% accurate about timeouts we should use task.run() here without await and then await the // taskcompletionsource right after but if we went down this path we would also have to account for exceptions thus complicating // the code considerably for little to no practical gain considering that the native call has trivial setup code and is very fast @@ -386,9 +510,7 @@ void FileUploader_FatalErrorOccurred_(object sender, FatalErrorOccurredEventArgs Func openCallback => await openCallback().ReadBytesAsync(disposeStream: autodisposeStream_), Func> openAsyncCallback => await (await openAsyncCallback()).ReadBytesAsync(disposeStream: autodisposeStream_), -#if NETCOREAPP - Func> openAsyncCallback => await (await openAsyncCallback()).ReadBytesAsync(disposeStream: autodisposeStream_), //only supported in the netcore era -#endif + Func> openAsyncCallback => await (await openAsyncCallback()).ReadBytesAsync(disposeStream: autodisposeStream_), byte[] dataByteArray => dataByteArray, IEnumerable dataEnumerableBytes => dataEnumerableBytes.ToArray(), //just in case @@ -397,21 +519,34 @@ void FileUploader_FatalErrorOccurred_(object sender, FatalErrorOccurredEventArgs }; } - void IFileUploaderEventEmittable.OnCancelled(CancelledEventArgs ea) => _cancelled?.Invoke(this, ea); - void IFileUploaderEventEmittable.OnLogEmitted(LogEmittedEventArgs ea) => _logEmitted?.Invoke(this, ea); - void IFileUploaderEventEmittable.OnStateChanged(StateChangedEventArgs ea) => _stateChanged?.Invoke(this, ea); - void IFileUploaderEventEmittable.OnFileUploaded(FileUploadedEventArgs ea) => _fileUploaded?.Invoke(this, ea); - void IFileUploaderEventEmittable.OnBusyStateChanged(BusyStateChangedEventArgs ea) => _busyStateChanged?.Invoke(this, ea); - void IFileUploaderEventEmittable.OnFatalErrorOccurred(FatalErrorOccurredEventArgs ea) => _fatalErrorOccurred?.Invoke(this, ea); - void IFileUploaderEventEmittable.OnFileUploadProgressPercentageAndDataThroughputChanged(FileUploadProgressPercentageAndDataThroughputChangedEventArgs ea) => _fileUploadProgressPercentageAndDataThroughputChanged?.Invoke(this, ea); + void IFileUploaderEventEmittable.OnCancelled(CancelledEventArgs ea) => OnCancelled(ea); + void IFileUploaderEventEmittable.OnCancelling(CancellingEventArgs ea) => OnCancelling(ea); + void IFileUploaderEventEmittable.OnLogEmitted(LogEmittedEventArgs ea) => OnLogEmitted(ea); + void IFileUploaderEventEmittable.OnStateChanged(StateChangedEventArgs ea) => OnStateChanged(ea); + void IFileUploaderEventEmittable.OnFileUploaded(FileUploadedEventArgs ea) => OnFileUploaded(ea); + void IFileUploaderEventEmittable.OnBusyStateChanged(BusyStateChangedEventArgs ea) => OnBusyStateChanged(ea); + void IFileUploaderEventEmittable.OnFatalErrorOccurred(FatalErrorOccurredEventArgs ea) => OnFatalErrorOccurred(ea); + void IFileUploaderEventEmittable.OnFileUploadProgressPercentageAndDataThroughputChanged(FileUploadProgressPercentageAndDataThroughputChangedEventArgs ea) => OnFileUploadProgressPercentageAndDataThroughputChanged(ea); + + private void OnCancelled(CancelledEventArgs ea) => _cancelled?.Invoke(this, ea); + private void OnCancelling(CancellingEventArgs ea) => _cancelling?.Invoke(this, ea); + private void OnLogEmitted(LogEmittedEventArgs ea) => _logEmitted?.Invoke(this, ea); + private void OnFileUploaded(FileUploadedEventArgs ea) => _fileUploaded?.Invoke(this, ea); + private void OnStateChanged(StateChangedEventArgs ea) => _stateChanged?.Invoke(this, ea); + private void OnBusyStateChanged(BusyStateChangedEventArgs ea) => _busyStateChanged?.Invoke(this, ea); + private void OnFatalErrorOccurred(FatalErrorOccurredEventArgs ea) => _fatalErrorOccurred?.Invoke(this, ea); + private void OnFileUploadProgressPercentageAndDataThroughputChanged(FileUploadProgressPercentageAndDataThroughputChangedEventArgs ea) => _fileUploadProgressPercentageAndDataThroughputChanged?.Invoke(this, ea); //this sort of approach proved to be necessary for our testsuite to be able to effectively mock away the INativeFileUploaderProxy internal class GenericNativeFileUploaderCallbacksProxy : INativeFileUploaderCallbacksProxy { public IFileUploaderEventEmittable FileUploader { get; set; } - public void CancelledAdvertisement() - => FileUploader?.OnCancelled(new CancelledEventArgs()); + public void CancelledAdvertisement(string reason = "") + => FileUploader?.OnCancelled(new CancelledEventArgs(reason)); + + public void CancellingAdvertisement(string reason = "") + => FileUploader?.OnCancelling(new CancellingEventArgs(reason)); public void LogMessageAdvertisement(string message, string category, ELogLevel level, string resource) => FileUploader?.OnLogEmitted(new LogEmittedEventArgs( @@ -437,20 +572,20 @@ public void FileUploadedAdvertisement(string resource) public void FatalErrorOccurredAdvertisement( string resource, string errorMessage, - EMcuMgrErrorCode mcuMgrErrorCode, - EFileUploaderGroupReturnCode fileUploaderGroupReturnCode + EGlobalErrorCode globalErrorCode ) => FileUploader?.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs( resource, errorMessage, - mcuMgrErrorCode, - fileUploaderGroupReturnCode + globalErrorCode )); - public void FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput) - => FileUploader?.OnFileUploadProgressPercentageAndDataThroughputChanged(new FileUploadProgressPercentageAndDataThroughputChangedEventArgs( - averageThroughput: averageThroughput, - progressPercentage: progressPercentage - )); + public void FileUploadProgressPercentageAndDataThroughputChangedAdvertisement( + int progressPercentage, + float averageThroughput + ) => FileUploader?.OnFileUploadProgressPercentageAndDataThroughputChanged(new FileUploadProgressPercentageAndDataThroughputChangedEventArgs( + averageThroughput: averageThroughput, + progressPercentage: progressPercentage + )); } } -} \ No newline at end of file +} diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Enums/EFirmwareErasureInitializationVerdict.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Enums/EFirmwareErasureInitializationVerdict.cs new file mode 100644 index 00000000..a12b6312 --- /dev/null +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Enums/EFirmwareErasureInitializationVerdict.cs @@ -0,0 +1,9 @@ +namespace Laerdal.McuMgr.FirmwareEraser.Contracts.Enums +{ + public enum EFirmwareErasureInitializationVerdict + { + Success = 0, + FailedErrorUponCommencing = 1, + FailedOtherErasureAlreadyInProgress = 2, + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Events/FatalErrorOccurredEventArgs.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Events/FatalErrorOccurredEventArgs.cs index d80de914..22f86913 100644 --- a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Events/FatalErrorOccurredEventArgs.cs +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Events/FatalErrorOccurredEventArgs.cs @@ -1,6 +1,7 @@ // ReSharper disable MemberCanBePrivate.Global // ReSharper disable ClassNeverInstantiated.Global +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Events; namespace Laerdal.McuMgr.FirmwareEraser.Contracts.Events @@ -8,10 +9,12 @@ namespace Laerdal.McuMgr.FirmwareEraser.Contracts.Events public readonly struct FatalErrorOccurredEventArgs : IMcuMgrEventArgs { public string ErrorMessage { get; } - - public FatalErrorOccurredEventArgs(string errorMessage) + public EGlobalErrorCode GlobalErrorCode { get; } + + public FatalErrorOccurredEventArgs(string errorMessage, EGlobalErrorCode globalErrorCode) { ErrorMessage = errorMessage; + GlobalErrorCode = globalErrorCode; } } } diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Exceptions/FirmwareErasureErroredOutException.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Exceptions/FirmwareErasureErroredOutException.cs index 6aa8a911..e37f83b7 100644 --- a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Exceptions/FirmwareErasureErroredOutException.cs +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Exceptions/FirmwareErasureErroredOutException.cs @@ -1,11 +1,15 @@ using System; +using Laerdal.McuMgr.Common.Enums; namespace Laerdal.McuMgr.FirmwareEraser.Contracts.Exceptions { public class FirmwareErasureErroredOutException : Exception, IFirmwareEraserException { - public FirmwareErasureErroredOutException(string errorMessage) : base($"An error occurred while erasing firmware: '{errorMessage}'") + public EGlobalErrorCode GlobalErrorCode { get; } + + public FirmwareErasureErroredOutException(string errorMessage, EGlobalErrorCode globalErrorCode) : base($"An error occurred while erasing firmware: '{errorMessage}'") { + GlobalErrorCode = globalErrorCode; } public FirmwareErasureErroredOutException(string errorMessage, Exception innerException) : base($"An error occurred while erasing firmware: '{errorMessage}'", innerException) diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/IFirmwareEraserCommandable.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/IFirmwareEraserCommandable.cs index 59828196..db9965ee 100644 --- a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/IFirmwareEraserCommandable.cs +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/IFirmwareEraserCommandable.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Laerdal.McuMgr.FirmwareEraser.Contracts.Enums; namespace Laerdal.McuMgr.FirmwareEraser.Contracts { @@ -18,7 +19,7 @@ public interface IFirmwareEraserCommandable /// Starts the erasure process on the firmware-image specified. /// /// The zero-based index of the firmware image to delete. By default it's 1 which is the index of the inactive firmware image. - void BeginErasure(int imageIndex = 1); + EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex = 1); /// Drops the active bluetooth-connection to the Zephyr device. void Disconnect(); diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCallbacksProxy.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCallbacksProxy.cs index d4a6e922..18e5ad91 100644 --- a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCallbacksProxy.cs +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCallbacksProxy.cs @@ -10,6 +10,6 @@ internal interface INativeFirmwareEraserCallbacksProxy void LogMessageAdvertisement(string message, string category, ELogLevel level); void StateChangedAdvertisement(EFirmwareErasureState oldState, EFirmwareErasureState newState); void BusyStateChangedAdvertisement(bool busyNotIdle); - void FatalErrorOccurredAdvertisement(string errorMessage); + void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode); } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCommandableProxy.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCommandableProxy.cs index 61cceedb..418920f6 100644 --- a/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCommandableProxy.cs +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/Contracts/Native/INativeFirmwareEraserCommandableProxy.cs @@ -1,8 +1,10 @@ -namespace Laerdal.McuMgr.FirmwareEraser.Contracts.Native +using Laerdal.McuMgr.FirmwareEraser.Contracts.Enums; + +namespace Laerdal.McuMgr.FirmwareEraser.Contracts.Native { internal interface INativeFirmwareEraserCommandableProxy { void Disconnect(); - void BeginErasure(int imageIndex); + EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex); } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs index c2504060..b53d2d2d 100644 --- a/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs @@ -42,8 +42,8 @@ public void StateChangedAdvertisement(EFirmwareErasureState oldState, EFirmwareE public void BusyStateChangedAdvertisement(bool busyNotIdle) => FirmwareEraser.OnBusyStateChanged(new BusyStateChangedEventArgs(busyNotIdle)); - public void FatalErrorOccurredAdvertisement(string errorMessage) - => FirmwareEraser.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(errorMessage)); + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) + => FirmwareEraser.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(errorMessage, globalErrorCode)); } private readonly INativeFirmwareEraserProxy _nativeFirmwareEraserProxy; @@ -58,8 +58,15 @@ internal FirmwareEraser(INativeFirmwareEraserProxy nativeFirmwareEraserProxy) public string LastFatalErrorMessage => _nativeFirmwareEraserProxy?.LastFatalErrorMessage; public void Disconnect() => _nativeFirmwareEraserProxy?.Disconnect(); - public void BeginErasure(int imageIndex = 1) => _nativeFirmwareEraserProxy?.BeginErasure(imageIndex); + public EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex = 1) + { + if (_nativeFirmwareEraserProxy == null) + throw new InvalidOperationException("The native firmware eraser is not initialized"); + + return _nativeFirmwareEraserProxy.BeginErasure(imageIndex); + } + private event EventHandler _logEmitted; private event EventHandler _stateChanged; private event EventHandler _busyStateChanged; @@ -111,10 +118,12 @@ public async Task EraseAsync(int imageIndex = 1, int timeoutInMs = -1) try { - StateChanged += EraseAsyncOnStateChanged; - FatalErrorOccurred += EraseAsyncOnFatalErrorOccurred; + StateChanged += FirmwareEraser_StateChanged_; + FatalErrorOccurred += FirmwareEraser_FatalErrorOccurred_; - BeginErasure(imageIndex); //00 dont use task.run here for now + var verdict = BeginErasure(imageIndex); //00 dont use task.run here for now + if (verdict != EFirmwareErasureInitializationVerdict.Success) + throw new ArgumentException(verdict.ToString()); _ = timeoutInMs <= 0 ? await taskCompletionSource.Task @@ -144,30 +153,27 @@ ex is not ArgumentException //10 wops probably missing native lib symbols! } finally { - StateChanged -= EraseAsyncOnStateChanged; - FatalErrorOccurred -= EraseAsyncOnFatalErrorOccurred; + StateChanged -= FirmwareEraser_StateChanged_; + FatalErrorOccurred -= FirmwareEraser_FatalErrorOccurred_; } return; - void EraseAsyncOnStateChanged(object sender, StateChangedEventArgs ea) + void FirmwareEraser_StateChanged_(object _, StateChangedEventArgs ea_) { - if (ea.NewState != EFirmwareErasureState.Complete) + if (ea_.NewState != EFirmwareErasureState.Complete) return; taskCompletionSource.TrySetResult(true); } - void EraseAsyncOnFatalErrorOccurred(object sender, FatalErrorOccurredEventArgs ea) + void FirmwareEraser_FatalErrorOccurred_(object _, FatalErrorOccurredEventArgs ea_) { - var isAboutUnauthorized = ea.ErrorMessage?.ToUpperInvariant().Contains("UNRECOGNIZED (11)") ?? false; //just in case - if (isAboutUnauthorized) + taskCompletionSource.TrySetException(ea_.GlobalErrorCode switch { - taskCompletionSource.TrySetException(new UnauthorizedException(ea.ErrorMessage)); - return; - } - - taskCompletionSource.TrySetException(new FirmwareErasureErroredOutException(ea.ErrorMessage)); //generic + EGlobalErrorCode.McuMgrErrorBeforeSmpV2_AccessDenied => new UnauthorizedException(ea_.ErrorMessage, ea_.GlobalErrorCode), //just in case + _ => new FirmwareErasureErroredOutException(ea_.ErrorMessage, ea_.GlobalErrorCode) + }); } //00 we are aware that in order to be 100% accurate about timeouts we should use task.run() here without await and then await the diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Enums/EFirmwareInstallerFatalErrorType.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Enums/EFirmwareInstallerFatalErrorType.cs index 6c493dd1..cfa94770 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Enums/EFirmwareInstallerFatalErrorType.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Enums/EFirmwareInstallerFatalErrorType.cs @@ -8,5 +8,6 @@ public enum EFirmwareInstallerFatalErrorType //these must mirror the enum values DeploymentFailed = 3, FirmwareImageSwapTimeout = 4, FirmwareUploadingErroredOut = 5, + FailedInstallationAlreadyInProgress = 6, } -} \ No newline at end of file +} diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Events/FatalErrorOccurredEventArgs.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Events/FatalErrorOccurredEventArgs.cs index e186bc73..97be44a5 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Events/FatalErrorOccurredEventArgs.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Events/FatalErrorOccurredEventArgs.cs @@ -1,6 +1,7 @@ // ReSharper disable MemberCanBePrivate.Global // ReSharper disable ClassNeverInstantiated.Global +using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Events; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; @@ -9,14 +10,16 @@ namespace Laerdal.McuMgr.FirmwareInstaller.Contracts.Events public readonly struct FatalErrorOccurredEventArgs : IMcuMgrEventArgs { public string ErrorMessage { get; } + public EGlobalErrorCode GlobalErrorCode { get; } public EFirmwareInstallationState State { get; } //the state in which the error occurred public EFirmwareInstallerFatalErrorType FatalErrorType { get; } - public FatalErrorOccurredEventArgs(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage) + public FatalErrorOccurredEventArgs(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, EGlobalErrorCode globalErrorCode) { State = state; ErrorMessage = errorMessage; FatalErrorType = fatalErrorType; + GlobalErrorCode = globalErrorCode; } } } diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationConfirmationStageTimeoutException.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationConfirmationStageTimeoutException.cs index aa1f8d9d..2e3eaeb9 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationConfirmationStageTimeoutException.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationConfirmationStageTimeoutException.cs @@ -1,11 +1,16 @@ // ReSharper disable RedundantExtendsListEntry +using Laerdal.McuMgr.Common.Enums; + namespace Laerdal.McuMgr.FirmwareInstaller.Contracts.Exceptions { public class FirmwareInstallationConfirmationStageTimeoutException : FirmwareInstallationErroredOutException, IFirmwareInstallationException { - public FirmwareInstallationConfirmationStageTimeoutException(int? estimatedSwapTimeInMilliseconds) - : base($"Device didn't confirm the new firmware within the given timeframe of {estimatedSwapTimeInMilliseconds} milliseconds. The new firmware will only last for just one power-cycle of the device.") + public FirmwareInstallationConfirmationStageTimeoutException(int? estimatedSwapTimeInMilliseconds, EGlobalErrorCode eaGlobalErrorCode) + : base( + errorMessage: $"Device didn't confirm the new firmware within the given timeframe of {estimatedSwapTimeInMilliseconds} milliseconds. The new firmware will only last for just one power-cycle of the device.", + globalErrorCode: eaGlobalErrorCode + ) { } } diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationErroredOutException.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationErroredOutException.cs index db3d70c7..1016445f 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationErroredOutException.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationErroredOutException.cs @@ -1,14 +1,20 @@ using System; +using Laerdal.McuMgr.Common.Enums; namespace Laerdal.McuMgr.FirmwareInstaller.Contracts.Exceptions { public class FirmwareInstallationErroredOutException : Exception, IFirmwareInstallationException { - public FirmwareInstallationErroredOutException(string errorMessage) : base($"An error occurred during firmware installation: '{errorMessage}'") + public EGlobalErrorCode GlobalErrorCode { get; } = EGlobalErrorCode.Unset; + + public FirmwareInstallationErroredOutException(string errorMessage, EGlobalErrorCode globalErrorCode = EGlobalErrorCode.Unset) + : base($"An error occurred during firmware installation: '{errorMessage}' (globalErrorCode={globalErrorCode})") { + GlobalErrorCode = globalErrorCode; } - - public FirmwareInstallationErroredOutException(string errorMessage, Exception innerException) : base($"An error occurred during firmware installation: '{errorMessage}'", innerException) + + public FirmwareInstallationErroredOutException(string errorMessage, Exception innerException) + : base($"An error occurred during firmware installation: '{errorMessage}'", innerException) { } } diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs index 9d4442c2..4d728360 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs @@ -1,9 +1,11 @@ +using Laerdal.McuMgr.Common.Enums; + namespace Laerdal.McuMgr.FirmwareInstaller.Contracts.Exceptions { public class FirmwareInstallationUploadingStageErroredOutException : FirmwareInstallationErroredOutException, IFirmwareInstallationException { - public FirmwareInstallationUploadingStageErroredOutException() - : base("An error occurred while uploading the firmware") + public FirmwareInstallationUploadingStageErroredOutException(EGlobalErrorCode globalErrorCode) + : base("An error occurred while uploading the firmware", globalErrorCode) { } } diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/IFirmwareInstallerCommandable.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/IFirmwareInstallerCommandable.cs index ef654b00..6a92de6a 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/IFirmwareInstallerCommandable.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/IFirmwareInstallerCommandable.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Laerdal.McuMgr.Common.Constants; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Exceptions; @@ -7,16 +8,26 @@ namespace Laerdal.McuMgr.FirmwareInstaller.Contracts public interface IFirmwareInstallerCommandable { /// - /// Begins the firmware upgrade process. To really know when the upgrade process has been completed you have to employ the progressPercentage methods. + /// Begins the firmware upgrade process. To really know when the upgrade process has been completed you have to listen to the associated events of the facade. /// + /// + /// When 'maxTriesCount' is greater than or equal to 2 the connection will be monitored in terms of how stable and reliable it is during the firmware-uploading stage and if + /// the uploading phase fails from the third attempt onwards then in the subsequent attempts the fail-safe settings in + /// and will be enforced to try to upload the firmware. + /// /// The firmware bytes. If zipped then the archive must contain the .bin file and not a directory. + /// The model of the host-device. + /// The manufacturer of the host-device /// The firmware upgrade mode. Best to leave this to the default value 'TestAndConfirm'. /// Specifies whether preexisting settings should be erased or not. /// In rF52840, due to how the flash memory works, requires ~20 sec to erase images. /// For Laerdal AEDs the recommended time is ~50secs. Adjust the time accordingly for each device you're testing. + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification - /// confirming each packet. This value should be lower or equal to MCUMGR_BUF_COUNT + /// confirming each packet. This value should be lower than or equal to MCUMGR_BUF_COUNT /// (https://github.com/zephyrproject-rtos/zephyr/blob/bd4ddec0c8c822bbdd420bd558b62c1d1a532c16/subsys/mgmt/mcumgr/Kconfig#L550) /// parameter in KConfig in NCS / Zephyr configuration and should also be supported on Mynewt devices. Mind, that in Zephyr, /// before https://github.com/zephyrproject-rtos/zephyr/pull/41959 was merged, the device required data to be sent with memory alignment. @@ -33,27 +44,35 @@ public interface IFirmwareInstallerCommandable /// The time to wait (in milliseconds) for a cancellation request to be properly handled. If this timeout expires then the mechanism will bail out forcefully without waiting for the underlying native code to cleanup properly. Task InstallAsync( byte[] data, + string hostDeviceModel, + string hostDeviceManufacturer, EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, - int? windowCapacity = null, - int? memoryAlignment = null, - int? pipelineDepth = null, - int? byteAlignment = null, + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null, // android only + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only int timeoutInMs = -1, int maxTriesCount = 10, int sleepTimeBetweenRetriesInMs = 100, int gracefulCancellationTimeoutInMs = 2_500 ); - + /// /// Begins the firmware upgrade process. To really know when the upgrade process has been completed you have to employ the progressPercentage methods. /// /// The firmware bytes. If zipped then the archive must contain the .bin file and not a directory. + /// The model of the host-device. + /// The manufacturer of the host-device. /// The firmware upgrade mode. Best to leave this to the default value 'TestAndConfirm'. /// Specifies whether preexisting settings should be erased or not. /// In rF52840, due to how the flash memory works, requires ~20 sec to erase images. - /// For Laerdal AEDs the recommended time is ~50secs. Adjust the time accordingly for each device you're testing. + /// For Laerdal AEDs the recommended time is ~50secs. Adjust the time accordingly for each device you're testing. + /// (Android only) Set the initial MTU size for the connection employed by the firmware-installation + /// (useful for some problematic devices such as Samsung A8 tablets). Acceptable custom values must lay within the range [23, 517]. + /// If null, zero or negative it will default to 498. Note that in quirky devices like Samsung Galaxy A8 the only value that works is 23 - anything else fails. /// (Android only) Set the window capacity. Values > 1 enable a new implementation for uploading /// the images, which makes use of SMP pipelining feature. The app will send this many packets immediately, without waiting for notification /// confirming each packet. This value should be lower or equal to MCUMGR_BUF_COUNT @@ -69,9 +88,12 @@ Task InstallAsync( /// to predict offset jumps as multiple packets are sent in parallel. EFirmwareInstallationVerdict BeginInstallation( byte[] data, + string hostDeviceModel, + string hostDeviceManufacturer, EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, int? windowCapacity = null, int? memoryAlignment = null, int? pipelineDepth = null, @@ -82,7 +104,7 @@ EFirmwareInstallationVerdict BeginInstallation( /// Cancels the firmware upgrade process /// void Cancel(); - + /// /// Disconnects the firmware installer from the targeted device /// diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCallbacksProxy.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCallbacksProxy.cs index f6e707f4..9831e16f 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCallbacksProxy.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCallbacksProxy.cs @@ -11,7 +11,7 @@ internal interface INativeFirmwareInstallerCallbacksProxy void LogMessageAdvertisement(string message, string category, ELogLevel level, string resource); void StateChangedAdvertisement(EFirmwareInstallationState oldState, EFirmwareInstallationState newState); void BusyStateChangedAdvertisement(bool busyNotIdle); - void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage); + void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, EGlobalErrorCode globalErrorCode); void FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput); } } \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCommandableProxy.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCommandableProxy.cs index caa7d8fe..d2b6ba04 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCommandableProxy.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Native/INativeFirmwareInstallerCommandableProxy.cs @@ -14,6 +14,7 @@ EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, // android only not applicable for ios int? windowCapacity = null, // android only not applicable for ios int? memoryAlignment = null, // android only not applicable for ios int? pipelineDepth = null, // ios only not applicable for android diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs index 0ffc949d..b80a7254 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Laerdal.McuMgr.Common.Constants; using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Events; using Laerdal.McuMgr.Common.Exceptions; @@ -39,9 +40,12 @@ public void Dispose() public EFirmwareInstallationVerdict BeginInstallation( byte[] data, + string hostDeviceModel, + string hostDeviceManufacturer, EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, // android only not applicable for ios int? windowCapacity = null, // android only not applicable for ios int? memoryAlignment = null, // android only not applicable for ios int? pipelineDepth = null, // ios only not applicable for android @@ -51,16 +55,46 @@ public EFirmwareInstallationVerdict BeginInstallation( if (data == null || !data.Any()) throw new ArgumentException("The data byte-array parameter is null or empty", nameof(data)); + var failsafeConnectionSettings = ConnectionSettingsHelpers.GetFailSafeConnectionSettingsIfHostDeviceIsProblematic( + uploadingNotDownloading: true, + + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + + pipelineDepth: pipelineDepth, + byteAlignment: byteAlignment, + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment + ); + if (failsafeConnectionSettings != null) + { + pipelineDepth = failsafeConnectionSettings.Value.pipelineDepth; + byteAlignment = failsafeConnectionSettings.Value.byteAlignment; + initialMtuSize = failsafeConnectionSettings.Value.initialMtuSize; + windowCapacity = failsafeConnectionSettings.Value.windowCapacity; + memoryAlignment = failsafeConnectionSettings.Value.memoryAlignment; + + OnLogEmitted(new LogEmittedEventArgs( + level: ELogLevel.Warning, + message: $"[FI.BI.010] Host device '{hostDeviceModel} (made by {hostDeviceManufacturer})' is known to be problematic. Resorting to using failsafe settings " + + $"(pipelineDepth={pipelineDepth?.ToString() ?? "null"}, byteAlignment={byteAlignment?.ToString() ?? "null"}, initialMtuSize={initialMtuSize?.ToString() ?? "null"}, windowCapacity={windowCapacity?.ToString() ?? "null"}, memoryAlignment={memoryAlignment?.ToString() ?? "null"})", + resource: "File", + category: "FileDownloader" + )); + } + _nativeFirmwareInstallerProxy.Nickname = "Firmware Installation"; //todo get this from a parameter var verdict = _nativeFirmwareInstallerProxy.BeginInstallation( data: data, mode: mode, - eraseSettings: eraseSettings ?? false, + eraseSettings: eraseSettings, pipelineDepth: pipelineDepth, byteAlignment: byteAlignment, - windowCapacity: windowCapacity ?? -1, - memoryAlignment: memoryAlignment ?? -1, - estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds ?? -1 + initialMtuSize: initialMtuSize, + windowCapacity: windowCapacity, + memoryAlignment: memoryAlignment, + estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds ); return verdict; @@ -151,13 +185,16 @@ public event EventHandler(state: false); try { - Cancelled += FirmwareInstallationAsyncOnCancelled; - StateChanged += FirmwareInstallationAsyncOnStateChanged; - FatalErrorOccurred += FirmwareInstallationAsyncOnFatalErrorOccurred; + Cancelled += FirmwareInstaller_Cancelled_; + StateChanged += FirmwareInstaller_StateChanged_; + FatalErrorOccurred += FirmwareInstaller_FatalErrorOccurred_; + + var failSafeSettingsToApply = ConnectionSettingsHelpers.GetFailsafeConnectionSettingsIfConnectionProvedToBeUnstable( + uploadingNotDownloading: true, + triesCount: triesCount, + maxTriesCount: maxTriesCount, + suspiciousTransportFailuresCount: suspiciousTransportFailuresCount + ); + if (failSafeSettingsToApply != null) + { + byteAlignment = failSafeSettingsToApply.Value.byteAlignment; + pipelineDepth = failSafeSettingsToApply.Value.pipelineDepth; + initialMtuSize = failSafeSettingsToApply.Value.initialMtuSize; + windowCapacity = failSafeSettingsToApply.Value.windowCapacity; + memoryAlignment = failSafeSettingsToApply.Value.memoryAlignment; + + if (!didWarnOnceAboutUnstableConnection) + { + didWarnOnceAboutUnstableConnection = true; + OnLogEmitted(new LogEmittedEventArgs( + level: ELogLevel.Warning, + message: $"[FI.IA.010] Attempt#{triesCount}: Connection is too unstable for uploading the firmware to the target device. Subsequent tries will use failsafe parameters on the connection " + + $"just in case it helps (byteAlignment={byteAlignment}, pipelineDepth={pipelineDepth}, initialMtuSize={initialMtuSize}, windowCapacity={windowCapacity}, memoryAlignment={memoryAlignment})", + resource: "Firmware", + category: "FirmwareInstaller" + )); + } + } var verdict = BeginInstallation( //00 dont use task.run here for now data: data, + hostDeviceModel: hostDeviceModel, + hostDeviceManufacturer: hostDeviceManufacturer, + mode: mode, - pipelineDepth: pipelineDepth, - byteAlignment: byteAlignment, eraseSettings: eraseSettings, - windowCapacity: windowCapacity, - memoryAlignment: memoryAlignment, - estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds + estimatedSwapTimeInMilliseconds: estimatedSwapTimeInMilliseconds, + + pipelineDepth: pipelineDepth, // ios only + byteAlignment: byteAlignment, // ios only + + initialMtuSize: initialMtuSize, // android only + windowCapacity: windowCapacity, // android only + memoryAlignment: memoryAlignment // android only ); if (verdict != EFirmwareInstallationVerdict.Success) throw new ArgumentException(verdict.ToString()); @@ -212,6 +284,11 @@ public async Task InstallAsync( if (++triesCount > maxTriesCount) //order throw new AllFirmwareInstallationAttemptsFailedException(maxTriesCount, innerException: ex); + if (_fileUploadProgressEventsCount <= 10) + { + suspiciousTransportFailuresCount++; + } + if (sleepTimeBetweenRetriesInMs > 0) //order { await Task.Delay(sleepTimeBetweenRetriesInMs); @@ -234,21 +311,21 @@ ex is not ArgumentException //10 wops probably missing native lib symbols! } finally { - Cancelled -= FirmwareInstallationAsyncOnCancelled; - StateChanged -= FirmwareInstallationAsyncOnStateChanged; - FatalErrorOccurred -= FirmwareInstallationAsyncOnFatalErrorOccurred; + Cancelled -= FirmwareInstaller_Cancelled_; + StateChanged -= FirmwareInstaller_StateChanged_; + FatalErrorOccurred -= FirmwareInstaller_FatalErrorOccurred_; CleanupResourcesOfLastUpload(); } return; - void FirmwareInstallationAsyncOnCancelled(object sender, CancelledEventArgs ea) + void FirmwareInstaller_Cancelled_(object sender, CancelledEventArgs ea) { taskCompletionSource.TrySetException(new FirmwareInstallationCancelledException()); } - void FirmwareInstallationAsyncOnStateChanged(object sender, StateChangedEventArgs ea) + void FirmwareInstaller_StateChanged_(object sender, StateChangedEventArgs ea) { switch (ea.NewState) { @@ -285,31 +362,24 @@ void FirmwareInstallationAsyncOnStateChanged(object sender, StateChangedEventArg // getting called right above but if that takes too long we give the killing blow by calling OnCancelled() manually here } - void FirmwareInstallationAsyncOnFatalErrorOccurred(object sender, FatalErrorOccurredEventArgs ea) + void FirmwareInstaller_FatalErrorOccurred_(object _, FatalErrorOccurredEventArgs ea_) { - var isAboutUnauthorized = ea.ErrorMessage?.ToUpperInvariant().Contains("UNRECOGNIZED (11)") ?? false; - if (isAboutUnauthorized) - { - taskCompletionSource.TrySetException(new UnauthorizedException(ea.ErrorMessage)); - return; - } - - if (ea.FatalErrorType == EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut || ea.State == EFirmwareInstallationState.Uploading) + taskCompletionSource.TrySetException(ea_ switch { - taskCompletionSource.TrySetException(new FirmwareInstallationUploadingStageErroredOutException()); - return; - } + { GlobalErrorCode: EGlobalErrorCode.McuMgrErrorBeforeSmpV2_AccessDenied } + => new UnauthorizedException(ea_.ErrorMessage, ea_.GlobalErrorCode), - if (ea.FatalErrorType == EFirmwareInstallerFatalErrorType.FirmwareImageSwapTimeout) //can happen during the fw-confirmation phase which is the last phase - { - taskCompletionSource.TrySetException(new FirmwareInstallationConfirmationStageTimeoutException(estimatedSwapTimeInMilliseconds)); - return; - } + { FatalErrorType: EFirmwareInstallerFatalErrorType.FirmwareImageSwapTimeout } + => new FirmwareInstallationConfirmationStageTimeoutException(estimatedSwapTimeInMilliseconds, ea_.GlobalErrorCode), - taskCompletionSource.TrySetException(new FirmwareInstallationErroredOutException($"{ea.ErrorMessage} (state={ea.State})")); //generic errors fall through here + { FatalErrorType: EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut } or { State: EFirmwareInstallationState.Uploading } + => new FirmwareInstallationUploadingStageErroredOutException(ea_.GlobalErrorCode), + + _ => new FirmwareInstallationErroredOutException($"{ea_.ErrorMessage} (state={ea_.State})", ea_.GlobalErrorCode) + }); } } - + //00 we are aware that in order to be 100% accurate about timeouts we should use task.run() here without await and then await the // taskcompletionsource right after but if we went down this path we would also have to account for exceptions thus complicating // the code considerably for little to no practical gain considering that the native call has trivial setup code and is very fast @@ -363,8 +433,14 @@ private void OnStateChanged(StateChangedEventArgs ea) private int _fileUploadProgressEventsCount; private void OnFirmwareUploadProgressPercentageAndDataThroughputChanged(FirmwareUploadProgressPercentageAndDataThroughputChangedEventArgs ea) { - _fileUploadProgressEventsCount++; - _firmwareUploadProgressPercentageAndDataThroughputChanged?.Invoke(this, ea); + try + { + _fileUploadProgressEventsCount++; + } + finally + { + _firmwareUploadProgressPercentageAndDataThroughputChanged?.Invoke(this, ea); + } } //this sort of approach proved to be necessary for our testsuite to be able to effectively mock away the INativeFirmwareInstallerProxy @@ -394,8 +470,8 @@ public void StateChangedAdvertisement(EFirmwareInstallationState oldState, EFirm public void BusyStateChangedAdvertisement(bool busyNotIdle) => FirmwareInstaller?.OnBusyStateChanged(new BusyStateChangedEventArgs(busyNotIdle)); - public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage) - => FirmwareInstaller?.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(state, fatalErrorType, errorMessage)); + public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, EGlobalErrorCode globalErrorCode) + => FirmwareInstaller?.OnFatalErrorOccurred(new FatalErrorOccurredEventArgs(state, fatalErrorType, errorMessage, globalErrorCode)); public void FirmwareUploadProgressPercentageAndDataThroughputChangedAdvertisement(int progressPercentage, float averageThroughput) => FirmwareInstaller?.OnFirmwareUploadProgressPercentageAndDataThroughputChanged(new FirmwareUploadProgressPercentageAndDataThroughputChangedEventArgs( diff --git a/Laerdal.McuMgr/iOS/Common/HelpersIOS.cs b/Laerdal.McuMgr/iOS/Common/HelpersIOS.cs index 57d83fc5..a2a15e10 100644 --- a/Laerdal.McuMgr/iOS/Common/HelpersIOS.cs +++ b/Laerdal.McuMgr/iOS/Common/HelpersIOS.cs @@ -7,12 +7,15 @@ static internal class HelpersIOS { static internal ELogLevel TranslateEIOSLogLevel(string level) => level?.Trim().ToUpperInvariant() switch { - "D" => ELogLevel.Debug, - "V" => ELogLevel.Verbose, - "I" => ELogLevel.Info, - "A" => ELogLevel.Info, //application - "W" => ELogLevel.Warning, - "E" => ELogLevel.Error, + "T" or "TRACE" => ELogLevel.Trace, + "D" or "DEBUG" => ELogLevel.Debug, + "V" or "VERBOSE" => ELogLevel.Verbose, + "I" or "INFO" => ELogLevel.Info, + "N" or "NOTICE" => ELogLevel.Info, + "A" or "APPLICATION" => ELogLevel.Info, //application + "W" or "WARN" => ELogLevel.Warning, + "E" or "ERROR" => ELogLevel.Error, + "C" or "CRITICAL" => ELogLevel.Error, _ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown log-level value") }; } diff --git a/Laerdal.McuMgr/iOS/DeviceResetter/DeviceResetter.cs b/Laerdal.McuMgr/iOS/DeviceResetter/DeviceResetter.cs index 34fc6ee0..e1049d90 100644 --- a/Laerdal.McuMgr/iOS/DeviceResetter/DeviceResetter.cs +++ b/Laerdal.McuMgr/iOS/DeviceResetter/DeviceResetter.cs @@ -50,11 +50,16 @@ internal IOSNativeDeviceResetterProxy(CBPeripheral bluetoothDevice, INativeDevic public string LastFatalErrorMessage => _nativeDeviceResetter?.LastFatalErrorMessage; public void Disconnect() => _nativeDeviceResetter?.Disconnect(); - public void BeginReset() => _nativeDeviceResetter?.BeginReset(); - - #endregion + public EDeviceResetterInitializationVerdict BeginReset() + { + if (_nativeDeviceResetter == null) + throw new InvalidOperationException("The native device resetter is not initialized"); + + return TranslateEIOSDeviceResetterInitializationVerdict(_nativeDeviceResetter.BeginReset(keepThisDummyParameter: false)); + } + #endregion #region listener callbacks -> event emitters @@ -88,9 +93,13 @@ public void StateChangedAdvertisement(EDeviceResetterState oldState, EDeviceRese oldState: oldState ); - public override void FatalErrorOccurredAdvertisement(string errorMessage) + public override void FatalErrorOccurredAdvertisement(string errorMessage, nint globalErrorCode) + => FatalErrorOccurredAdvertisement(errorMessage, (EGlobalErrorCode)globalErrorCode); + + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) => _nativeResetterCallbacksProxy?.FatalErrorOccurredAdvertisement( - errorMessage: errorMessage + errorMessage: errorMessage, + globalErrorCode: globalErrorCode ); #endregion listener events @@ -105,6 +114,14 @@ public override void FatalErrorOccurredAdvertisement(string errorMessage) EIOSDeviceResetterState.Resetting => EDeviceResetterState.Resetting, _ => throw new ArgumentOutOfRangeException(nameof(state), state, "Unknown enum value") }; + + static private EDeviceResetterInitializationVerdict TranslateEIOSDeviceResetterInitializationVerdict(EIOSDeviceResetInitializationVerdict verdict) => verdict switch + { + EIOSDeviceResetInitializationVerdict.Success => EDeviceResetterInitializationVerdict.Success, + EIOSDeviceResetInitializationVerdict.FailedErrorUponCommencing => EDeviceResetterInitializationVerdict.FailedErrorUponCommencing, + EIOSDeviceResetInitializationVerdict.FailedOtherResetAlreadyInProgress => EDeviceResetterInitializationVerdict.FailedOtherResetAlreadyInProgress, + _ => throw new ArgumentOutOfRangeException(nameof(verdict), verdict, "Unknown enum value") + }; } } } \ No newline at end of file diff --git a/Laerdal.McuMgr/iOS/FileDownloader/FileDownloader.cs b/Laerdal.McuMgr/iOS/FileDownloader/FileDownloader.cs index 427eaede..ffdcdfc6 100644 --- a/Laerdal.McuMgr/iOS/FileDownloader/FileDownloader.cs +++ b/Laerdal.McuMgr/iOS/FileDownloader/FileDownloader.cs @@ -9,6 +9,7 @@ using Laerdal.McuMgr.FileDownloader.Contracts; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Native; +using Laerdal.McuMgr.FileUploader.Contracts.Enums; using McuMgrBindingsiOS; namespace Laerdal.McuMgr.FileDownloader @@ -80,6 +81,23 @@ private void CleanupInfrastructure() _nativeFileDownloader?.Dispose(); _nativeFileDownloader = null; } + + public bool TrySetContext(object context) + { + return true; //nothing to do in ios only android needs this and supports it + } + + public bool TrySetBluetoothDevice(object bluetoothDevice) + { + var iosBluetoothDevice = bluetoothDevice as CBPeripheral ?? throw new ArgumentException($"Expected {nameof(bluetoothDevice)} to be of type {nameof(CBPeripheral)}", nameof(bluetoothDevice)); + + return _nativeFileDownloader?.TrySetBluetoothDevice(iosBluetoothDevice) ?? false; + } + + public bool TryInvalidateCachedTransport() + { + return _nativeFileDownloader?.TryInvalidateCachedTransport() ?? false; + } #region commands @@ -88,9 +106,14 @@ private void CleanupInfrastructure() public void Cancel() => _nativeFileDownloader?.Cancel(); public void Disconnect() => _nativeFileDownloader?.Disconnect(); - - public EFileDownloaderVerdict BeginDownload(string remoteFilePath) - => TranslateFileDownloaderVerdict(_nativeFileDownloader.BeginDownload(remoteFilePath)); + + public EFileDownloaderVerdict BeginDownload( + string remoteFilePath, + int? initialMtuSize = null // android only + ) + { + return TranslateFileDownloaderVerdict(_nativeFileDownloader.BeginDownload(remoteFilePath: remoteFilePath)); + } #endregion commands @@ -151,8 +174,18 @@ public override void DownloadCompletedAdvertisement(string resource, NSNumber[] public void DownloadCompletedAdvertisement(string resource, byte[] data) //conformance to the interface => _nativeFileDownloaderCallbacksProxy?.DownloadCompletedAdvertisement(resource, data); - public override void FatalErrorOccurredAdvertisement(string resource, string errorMessage) - => _nativeFileDownloaderCallbacksProxy?.FatalErrorOccurredAdvertisement(resource, errorMessage); + public override void FatalErrorOccurredAdvertisement( + string resource, + string errorMessage, + nint globalErrorCode + ) => FatalErrorOccurredAdvertisement( + resource, + errorMessage, + (EGlobalErrorCode)(int)globalErrorCode + ); + + public void FatalErrorOccurredAdvertisement(string resource, string errorMessage, EGlobalErrorCode globalErrorCode) + => _nativeFileDownloaderCallbacksProxy?.FatalErrorOccurredAdvertisement(resource, errorMessage, globalErrorCode); public override void FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement(nint progressPercentage, float averageThroughput) => FileDownloadProgressPercentageAndDataThroughputChangedAdvertisement( @@ -179,6 +212,11 @@ static private EFileDownloaderVerdict TranslateFileDownloaderVerdict(EIOSFileDow { return EFileDownloaderVerdict.FailedInvalidSettings; } + + if (verdict == EIOSFileDownloadingInitializationVerdict.FailedErrorUponCommencing) + { + return EFileDownloaderVerdict.FailedErrorUponCommencing; + } if (verdict == EIOSFileDownloadingInitializationVerdict.FailedDownloadAlreadyInProgress) { diff --git a/Laerdal.McuMgr/iOS/FileUploader/FileUploader.cs b/Laerdal.McuMgr/iOS/FileUploader/FileUploader.cs index 1b99a5fe..d1c6ba3a 100644 --- a/Laerdal.McuMgr/iOS/FileUploader/FileUploader.cs +++ b/Laerdal.McuMgr/iOS/FileUploader/FileUploader.cs @@ -50,7 +50,7 @@ internal IOSNativeFileUploaderProxy(CBPeripheral bluetoothDevice, INativeFileUpl public string LastFatalErrorMessage => _nativeFileUploader?.LastFatalErrorMessage; - public void Cancel() => _nativeFileUploader?.Cancel(); + public void Cancel(string reason = "") => _nativeFileUploader?.Cancel(reason); public void Disconnect() => _nativeFileUploader?.Disconnect(); // public new void Dispose() { ... } dont there is no need to override the base implementation @@ -99,20 +99,34 @@ public void CleanupResourcesOfLastUpload() //00 } private NSData _nsDataOfFileInCurrentlyActiveUpload; - public EFileUploaderVerdict BeginUpload(string remoteFilePath, byte[] data) + + public EFileUploaderVerdict BeginUpload( + string remoteFilePath, + byte[] data, + + int? pipelineDepth = null, // ios only + int? byteAlignment = null, // ios only + + int? initialMtuSize = null, // android only + int? windowCapacity = null, // android only + int? memoryAlignment = null // android only + ) { var nsDataOfFileToUpload = NSData.FromArray(data); var verdict = TranslateFileUploaderVerdict(_nativeFileUploader.BeginUpload( data: nsDataOfFileToUpload, - remoteFilePath: remoteFilePath + remoteFilePath: remoteFilePath, + + pipelineDepth: pipelineDepth ?? -1, + byteAlignment: byteAlignment ?? -1 )); if (verdict != EFileUploaderVerdict.Success) { nsDataOfFileToUpload.Dispose(); return verdict; } - + _nsDataOfFileInCurrentlyActiveUpload = nsDataOfFileToUpload; return EFileUploaderVerdict.Success; } @@ -146,8 +160,11 @@ public IFileUploaderEventEmittable FileUploader set => _nativeFileUploaderCallbacksProxy!.FileUploader = value; } - public override void CancelledAdvertisement() - => _nativeFileUploaderCallbacksProxy?.CancelledAdvertisement(); + public override void CancellingAdvertisement(string reason) + => _nativeFileUploaderCallbacksProxy?.CancellingAdvertisement(reason); + + public override void CancelledAdvertisement(string reason) + => _nativeFileUploaderCallbacksProxy?.CancelledAdvertisement(reason); public override void LogMessageAdvertisement(string message, string category, string level, string resource) => LogMessageAdvertisement( @@ -187,23 +204,20 @@ public override void BusyStateChangedAdvertisement(bool busyNotIdle) public override void FatalErrorOccurredAdvertisement( string resource, string errorMessage, - nint mcuMgrErrorCode + nint globalErrorCode ) => FatalErrorOccurredAdvertisement( resource, errorMessage, - (EMcuMgrErrorCode)(int)mcuMgrErrorCode, - EFileUploaderGroupReturnCode.Unset + (EGlobalErrorCode)(int)globalErrorCode ); public void FatalErrorOccurredAdvertisement( //conformance to the interface string resource, - string errorMessage, // ReSharper disable once MethodOverloadWithOptionalParameter - EMcuMgrErrorCode mcuMgrErrorCode, - EFileUploaderGroupReturnCode fileUploaderGroupReturnCode + string errorMessage, + EGlobalErrorCode globalErrorCode ) => _nativeFileUploaderCallbacksProxy?.FatalErrorOccurredAdvertisement( resource, errorMessage, - mcuMgrErrorCode, - fileUploaderGroupReturnCode + globalErrorCode ); public override void FileUploadProgressPercentageAndDataThroughputChangedAdvertisement(nint progressPercentage, float averageThroughput) @@ -225,17 +239,22 @@ static private EFileUploaderVerdict TranslateFileUploaderVerdict(EIOSFileUploadi { return EFileUploaderVerdict.Success; } + + if (verdict == EIOSFileUploadingInitializationVerdict.FailedInvalidData) + { + return EFileUploaderVerdict.FailedInvalidData; + } if (verdict == EIOSFileUploadingInitializationVerdict.FailedInvalidSettings) { return EFileUploaderVerdict.FailedInvalidSettings; } - if (verdict == EIOSFileUploadingInitializationVerdict.FailedInvalidData) + if (verdict == EIOSFileUploadingInitializationVerdict.FailedErrorUponCommencing) { - return EFileUploaderVerdict.FailedInvalidData; + return EFileUploaderVerdict.FailedErrorUponCommencing; } - + if (verdict == EIOSFileUploadingInitializationVerdict.FailedOtherUploadAlreadyInProgress) { return EFileUploaderVerdict.FailedOtherUploadAlreadyInProgress; diff --git a/Laerdal.McuMgr/iOS/FirmwareEraser/FirmwareEraser.cs b/Laerdal.McuMgr/iOS/FirmwareEraser/FirmwareEraser.cs index 8cd90a89..4a7942aa 100644 --- a/Laerdal.McuMgr/iOS/FirmwareEraser/FirmwareEraser.cs +++ b/Laerdal.McuMgr/iOS/FirmwareEraser/FirmwareEraser.cs @@ -50,7 +50,13 @@ internal IOSNativeFirmwareEraserProxy(CBPeripheral bluetoothDevice, INativeFirmw public void Disconnect() => _nativeFirmwareEraser?.Disconnect(); //we are simply forwarding the commands down to the native world of ios here public string LastFatalErrorMessage => _nativeFirmwareEraser?.LastFatalErrorMessage; - public void BeginErasure(int imageIndex) => _nativeFirmwareEraser?.BeginErasure(imageIndex); + public EFirmwareErasureInitializationVerdict BeginErasure(int imageIndex) + { + if (_nativeFirmwareEraser == null) + throw new InvalidOperationException("The native firmware eraser is not initialized"); + + return TranslateEIOSFirmwareErasureInitializationVerdict(_nativeFirmwareEraser.BeginErasure(imageIndex)); + } #endregion @@ -88,8 +94,13 @@ public void StateChangedAdvertisement(EFirmwareErasureState oldState, EFirmwareE oldState: oldState ); - public override void BusyStateChangedAdvertisement(bool busyNotIdle) => _nativeFirmwareEraserCallbacksProxy?.BusyStateChangedAdvertisement(busyNotIdle); - public override void FatalErrorOccurredAdvertisement(string errorMessage) => _nativeFirmwareEraserCallbacksProxy?.FatalErrorOccurredAdvertisement(errorMessage); + public override void BusyStateChangedAdvertisement(bool busyNotIdle) + => _nativeFirmwareEraserCallbacksProxy?.BusyStateChangedAdvertisement(busyNotIdle); + + public override void FatalErrorOccurredAdvertisement(string errorMessage, nint globalErrorCode) + => FatalErrorOccurredAdvertisement(errorMessage, (EGlobalErrorCode)globalErrorCode); + public void FatalErrorOccurredAdvertisement(string errorMessage, EGlobalErrorCode globalErrorCode) + => _nativeFirmwareEraserCallbacksProxy?.FatalErrorOccurredAdvertisement(errorMessage, globalErrorCode); #endregion @@ -98,10 +109,19 @@ public void StateChangedAdvertisement(EFirmwareErasureState oldState, EFirmwareE { EIOSFirmwareEraserState.None => EFirmwareErasureState.None, EIOSFirmwareEraserState.Idle => EFirmwareErasureState.Idle, + EIOSFirmwareEraserState.Failed => EFirmwareErasureState.Failed, EIOSFirmwareEraserState.Erasing => EFirmwareErasureState.Erasing, EIOSFirmwareEraserState.Complete => EFirmwareErasureState.Complete, _ => throw new ArgumentOutOfRangeException(nameof(state), state, "Unknown enum value") }; + + static private EFirmwareErasureInitializationVerdict TranslateEIOSFirmwareErasureInitializationVerdict(EIOSFirmwareErasureInitializationVerdict verdict) => verdict switch + { + EIOSFirmwareErasureInitializationVerdict.Success => EFirmwareErasureInitializationVerdict.Success, + EIOSFirmwareErasureInitializationVerdict.FailedErrorUponCommencing => EFirmwareErasureInitializationVerdict.FailedErrorUponCommencing, + EIOSFirmwareErasureInitializationVerdict.FailedOtherErasureAlreadyInProgress => EFirmwareErasureInitializationVerdict.FailedOtherErasureAlreadyInProgress, + _ => throw new ArgumentOutOfRangeException(nameof(verdict), verdict, "Unknown enum value") + }; } } } \ No newline at end of file diff --git a/Laerdal.McuMgr/iOS/FirmwareInstaller/FirmwareInstaller.cs b/Laerdal.McuMgr/iOS/FirmwareInstaller/FirmwareInstaller.cs index e062c398..4d5b06fa 100644 --- a/Laerdal.McuMgr/iOS/FirmwareInstaller/FirmwareInstaller.cs +++ b/Laerdal.McuMgr/iOS/FirmwareInstaller/FirmwareInstaller.cs @@ -2,6 +2,7 @@ // ReSharper disable RedundantExtendsListEntry using System; +using System.Threading.Tasks; using CoreBluetooth; using Foundation; using Laerdal.McuMgr.Common; @@ -105,12 +106,16 @@ public EFirmwareInstallationVerdict BeginInstallation( EFirmwareInstallationMode mode = EFirmwareInstallationMode.TestAndConfirm, bool? eraseSettings = null, int? estimatedSwapTimeInMilliseconds = null, + int? initialMtuSize = null, //not applicable in ios int? windowCapacity = null, //not applicable in ios int? memoryAlignment = null, //not applicable in ios int? pipelineDepth = null, int? byteAlignment = null ) { + if (_nativeFirmwareInstaller == null) + throw new InvalidOperationException("The native firmware installer is not initialized"); + var nsDataOfFirmware = NSData.FromArray(data); var verdict = TranslateFirmwareInstallationVerdict(_nativeFirmwareInstaller.BeginInstallation( @@ -149,18 +154,20 @@ public IFirmwareInstallerEventEmittable FirmwareInstaller public override void CancelledAdvertisement() => _nativeFirmwareInstallerCallbacksProxy?.CancelledAdvertisement(); public override void BusyStateChangedAdvertisement(bool busyNotIdle) => _nativeFirmwareInstallerCallbacksProxy?.BusyStateChangedAdvertisement(busyNotIdle); - public override void FatalErrorOccurredAdvertisement(EIOSFirmwareInstallationState state, EIOSFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage) + public override void FatalErrorOccurredAdvertisement(EIOSFirmwareInstallationState state, EIOSFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, nint globalErrorCode) => FatalErrorOccurredAdvertisement( state: TranslateEIOSFirmwareInstallationState(state), errorMessage: errorMessage, - fatalErrorType: TranslateEIOSFirmwareInstallerFatalErrorType(fatalErrorType) + fatalErrorType: TranslateEIOSFirmwareInstallerFatalErrorType(fatalErrorType), + globalErrorCode: (EGlobalErrorCode) globalErrorCode ); - - public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage) //just to conform to the interface + + public void FatalErrorOccurredAdvertisement(EFirmwareInstallationState state, EFirmwareInstallerFatalErrorType fatalErrorType, string errorMessage, EGlobalErrorCode globalErrorCode) //just to conform to the interface => _nativeFirmwareInstallerCallbacksProxy?.FatalErrorOccurredAdvertisement( state: state, errorMessage: errorMessage, - fatalErrorType: fatalErrorType + fatalErrorType: fatalErrorType, + globalErrorCode: globalErrorCode ); public override void LogMessageAdvertisement(string message, string category, string level) @@ -213,6 +220,8 @@ static private EFirmwareInstallerFatalErrorType TranslateEIOSFirmwareInstallerFa EIOSFirmwareInstallerFatalErrorType.DeploymentFailed => EFirmwareInstallerFatalErrorType.DeploymentFailed, EIOSFirmwareInstallerFatalErrorType.FirmwareImageSwapTimeout => EFirmwareInstallerFatalErrorType.FirmwareImageSwapTimeout, EIOSFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut => EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut, + EIOSFirmwareInstallerFatalErrorType.FailedInstallationAlreadyInProgress => EFirmwareInstallerFatalErrorType.FailedInstallationAlreadyInProgress, + _ => throw new ArgumentOutOfRangeException(nameof(fatalErrorType), actualValue: fatalErrorType, message: "Unknown enum value") }; } diff --git a/Laerdal.Scripts/Laerdal.Builder.targets b/Laerdal.Scripts/Laerdal.Builder.targets index da8d8535..5ecd54b9 100644 --- a/Laerdal.Scripts/Laerdal.Builder.targets +++ b/Laerdal.Scripts/Laerdal.Builder.targets @@ -14,6 +14,11 @@ + + + + + @@ -30,8 +35,9 @@ %0A - Release - true + Release + false + false $(MSBuildThisFileDirectory) @@ -52,8 +58,6 @@ true true - false - @@ -86,6 +90,11 @@ + + + + + @@ -221,44 +230,59 @@ - - - - - + + - - + + - + + + + - - + + + + - + - + Properties="Configuration=$(Configuration);Should_Skip_MacCatalyst=$(Should_Skip_MacCatalyst);Laerdal_Bindings_iOS___DotnetTargetPlatformVersion=$(Laerdal_Bindings_iOS___DotnetTargetPlatformVersion);Laerdal_Bindings_Android___DotnetTargetPlatformVersion=$(Laerdal_Bindings_Android___DotnetTargetPlatformVersion);Laerdal_Bindings_MacCatalyst___DotnetTargetPlatformVersion=$(Laerdal_Bindings_MacCatalyst___DotnetTargetPlatformVersion);"/> - $(TestParameters) test 'Laerdal.McuMgr.Tests/Laerdal.McuMgr.Tests.csproj' - $(TestParameters) --logger 'trx;LogFileName=TEST-Laerdal.McuMgr.Tests.xml' - $(TestParameters) --verbosity '4' - $(TestParameters) --configuration '$(Configuration)' - $(TestParameters) --results-directory '$(Laerdal_Test_Results_Folderpath)' + $(TestParameters) test + $(TestParameters) 'Laerdal.McuMgr.Tests/Laerdal.McuMgr.Tests.csproj' + $(TestParameters) --logger 'trx;LogFileName=TEST-Laerdal.McuMgr.Tests.xml' + $(TestParameters) --no-restore + $(TestParameters) --verbosity '4' + $(TestParameters) --configuration '$(Configuration)' + $(TestParameters) --results-directory '$(Laerdal_Test_Results_Folderpath)' + $(TestParameters) -m:1 + $(TestParameters) -p:Should_Skip_MacCatalyst=$(Should_Skip_MacCatalyst) + $(TestParameters) -p:Laerdal_Bindings_iOS___DotnetTargetPlatformVersion=$(Laerdal_Bindings_iOS___DotnetTargetPlatformVersion) + $(TestParameters) -p:Laerdal_Bindings_Android___DotnetTargetPlatformVersion=$(Laerdal_Bindings_Android___DotnetTargetPlatformVersion) + $(TestParameters) -p:Laerdal_Bindings_MacCatalyst___DotnetTargetPlatformVersion=$(Laerdal_Bindings_MacCatalyst___DotnetTargetPlatformVersion)