diff --git a/CHANGELOG.md b/CHANGELOG.md index 782046c..7a2c493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ -## 6.2.1 -* [Android] Updated Nordic DFU Library to version 2.7.0 +## 7.0.0 +* All callbacks in startDfu() have been moved to DfuEventHandler class. +* AndroidSpecialOption() and IosSpecialOption() have been deprecated in favor of AndroidOptions() and DarwinOptions(). +* +* iOS and macOS implementation have been merged in one Darwin class. +* Added parallel DFU support (thanks @Flasher-MS !) +* [Android] Updated Nordic DFU Library to version 2.8.0 +* [Darwin] Updated Nordic DFU Library to version 4.16.0 ## 6.2.0 * [Android] Updated Nordic DFU Library to version 2.5.0 diff --git a/README.md b/README.md index a3a3d5e..4d06b09 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,34 @@ await NordicDfu().startDfu( ); ``` +## Parallel DFU + +Available from version 7.0.0 + +### Concurrent DFU Processes +- DFU operations can run simultaneously on multiple devices. +- Callbacks are triggered correctly and independently for each device. + +### Interface change +- Updated `abortDfu` method to include an optional `address` parameter: + - **If an address is provided:** The DFU process for the specified device will be aborted. **(iOS only)** + - **If no address is provided:** All active DFU processes will be aborted. +- Added error handling for `abortDfu`: + - `FlutterError("INVALID_ADDRESS")` is thrown if the provided address does not match any active DFU process. + - `FlutterError("NO_ACTIVE_DFU")` is thrown if no address is provided and there are no active DFU processes. + +### iOS +- ✅ Devices update in parallel. +- ✅ Callbacks set in `startDfu` are called independently for each device. +- ✅ All active DFU processes can be aborted using the `abortDfu` method without an `address`. +- ✅ DFU processes can be individually aborted using the `abortDfu` method with an `address`. + +### Android +- ✅ Devices update in parallel (set limit of 8). +- ✅ Callbacks set in `startDfu` are called independently for each device. +- ✅ All active DFU processes can be aborted using the `abortDfu` method without an `address`. +- ❌ DFU processes cannot be individually aborted using the `abortDfu` method with an `address` due to current limitations in the underlying [Android-DFU-Library](https://github.com/NordicSemiconductor/Android-DFU-Library). + ## Resources - [DFU Introduction](https://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v11.0.0/examples_ble_dfu.html?cp=6_0_0_4_3_1 "BLE Bootloader/DFU") diff --git a/android/src/main/kotlin/dev/steenbakker/nordicdfu/NordicDfuPlugin.kt b/android/src/main/kotlin/dev/steenbakker/nordicdfu/NordicDfuPlugin.kt index fe9ff07..20a69ed 100644 --- a/android/src/main/kotlin/dev/steenbakker/nordicdfu/NordicDfuPlugin.kt +++ b/android/src/main/kotlin/dev/steenbakker/nordicdfu/NordicDfuPlugin.kt @@ -28,7 +28,7 @@ private class DfuProcess( val serviceClass: Class ) -private val DFU_SERVICE_CLASSES = arrayListOf>( +private val DFU_SERVICE_CLASSES = arrayListOf( DfuService::class.java, DfuService2::class.java, DfuService3::class.java, @@ -77,10 +77,6 @@ class NordicDfuPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHan } override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - if (mContext != null) { - DfuServiceListenerHelper.registerProgressListener(mContext!!, mDfuProgressListener) - } - this.sink = events } @@ -234,6 +230,10 @@ class NordicDfuPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHan starter.setRebootTime(rebootTime) } + if (mContext != null) { + DfuServiceListenerHelper.registerProgressListener(mContext!!, mDfuProgressListener, address) + } + // fix notification on android 8 and above if (startAsForegroundService == null || startAsForegroundService) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !hasCreateNotification) { diff --git a/ios/.gitignore b/darwin/.gitignore similarity index 100% rename from ios/.gitignore rename to darwin/.gitignore diff --git a/ios/Assets/.gitkeep b/darwin/Assets/.gitkeep similarity index 100% rename from ios/Assets/.gitkeep rename to darwin/Assets/.gitkeep diff --git a/ios/Classes/SwiftNordicDfuPlugin.swift b/darwin/Classes/NordicDfuPlugin.swift similarity index 89% rename from ios/Classes/SwiftNordicDfuPlugin.swift rename to darwin/Classes/NordicDfuPlugin.swift index 497c905..86d2609 100644 --- a/ios/Classes/SwiftNordicDfuPlugin.swift +++ b/darwin/Classes/NordicDfuPlugin.swift @@ -1,21 +1,33 @@ -import Flutter -import UIKit import NordicDFU import CoreBluetooth -public class SwiftNordicDfuPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, LoggerDelegate { +#if os(iOS) + import Flutter + import UIKit +#else + import AppKit + import FlutterMacOS +#endif + +public class NordicDfuPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, LoggerDelegate { let registrar: FlutterPluginRegistrar private var sink: FlutterEventSink? private var activeDfuMap: [String: DfuProcess] = [:] public static func register(with registrar: FlutterPluginRegistrar) { - let instance = SwiftNordicDfuPlugin(registrar) + let instance = NordicDfuPlugin(registrar) + + #if os(iOS) + let messenger = registrar.messenger() + #else + let messenger = registrar.messenger + #endif - let method = FlutterMethodChannel(name: "dev.steenbakker.nordic_dfu/method", binaryMessenger: registrar.messenger()) + let method = FlutterMethodChannel(name: "dev.steenbakker.nordic_dfu/method", binaryMessenger: messenger) let event = FlutterEventChannel(name: - "dev.steenbakker.nordic_dfu/event", binaryMessenger: registrar.messenger()) + "dev.steenbakker.nordic_dfu/event", binaryMessenger: messenger) registrar.addMethodCallDelegate(instance, channel: method) event.setStreamHandler(instance) @@ -45,33 +57,28 @@ public class SwiftNordicDfuPlugin: NSObject, FlutterPlugin, FlutterStreamHandler } // Aborts ongoing DFU process(es) - // - // If `call.arguments["address"]` is `nil`, the method aborts all active DFU processes - // If `call.arguments["address"]` contains a specific address, the method aborts the DFU process for that address + // If `call.arguments["address"]` is nil, aborts all active DFU processes. + // If `call.arguments["address"]` contains an address, aborts the DFU process for that address. private func abortDfu(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - let arguments = call.arguments as? [String: Any] - let address = arguments?["address"] as? String - - if address == nil { + guard let arguments = call.arguments as? [String: Any], + let address = arguments["address"] as? String else { // Abort all DFU processes if activeDfuMap.isEmpty { result(FlutterError(code: "NO_ACTIVE_DFU", message: "No active DFU processes to abort", details: nil)) - return - } - for (_, process) in activeDfuMap { - process.controller?.abort() + } else { + activeDfuMap.values.forEach { _ = $0.controller?.abort() } // Explicitly ignore result of `abort()` + result(nil) } - result(nil) return } // Abort DFU process for the specified address - guard let process = activeDfuMap[address!] else { - result(FlutterError(code: "INVALID_ADDRESS", message: "No DFU process found for address: \(address!).", details: nil)) + guard let process = activeDfuMap[address] else { + result(FlutterError(code: "INVALID_ADDRESS", message: "No DFU process found for address: \(address).", details: nil)) return } - process.controller?.abort() + _ = process.controller?.abort() // Explicitly ignore result of `abort()` result(nil) } @@ -229,7 +236,7 @@ private class DfuProcess { deviceAddress: String, firmware: DFUFirmware, uuid: UUID, - delegate: SwiftNordicDfuPlugin, + delegate: NordicDfuPlugin, result: @escaping FlutterResult, options: DfuOptions ) { @@ -254,10 +261,10 @@ private class DfuProcess { // Handles DFU service and progress updates for a specific device public class DeviceScopedDFUDelegate: NSObject, DFUServiceDelegate, DFUProgressDelegate { - private let originalDelegate: SwiftNordicDfuPlugin + private let originalDelegate: NordicDfuPlugin private let deviceAddress: String - init(delegate: SwiftNordicDfuPlugin, deviceAddress: String) { + init(delegate: NordicDfuPlugin, deviceAddress: String) { self.originalDelegate = delegate self.deviceAddress = deviceAddress } diff --git a/ios/nordic_dfu.podspec b/darwin/nordic_dfu.podspec similarity index 68% rename from ios/nordic_dfu.podspec rename to darwin/nordic_dfu.podspec index a9c83a0..a8cbee7 100644 --- a/ios/nordic_dfu.podspec +++ b/darwin/nordic_dfu.podspec @@ -3,8 +3,8 @@ # Pod::Spec.new do |s| s.name = 'nordic_dfu' - s.version = '1.0.0' - s.summary = 'iOS DFU plugin for flutter.' + s.version = '2.0.0' + s.summary = 'DFU plugin for flutter.' s.description = <<-DESC A new flutter plugin project. DESC @@ -15,8 +15,10 @@ A new flutter plugin project. s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.swift_version = '5.4' - s.dependency 'Flutter' - s.dependency 'iOSDFULibrary', '~> 4.15.3' - s.ios.deployment_target = '9.0' + s.dependency 'NordicDFU', '~> 4.16.0' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.14' end diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 9d2bd61..0bc5ace 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,5 @@ -include: package:lint/strict.yaml \ No newline at end of file +include: package:very_good_analysis/analysis_options.yaml +analyzer: + errors: + lines_longer_than_80_chars: ignore + public_member_api_docs: ignore diff --git a/example/android/build.gradle b/example/android/build.gradle index 8614a9e..4d95bf7 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.2' + classpath 'com.android.tools.build:gradle:8.7.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index bfbc341..79ee9eb 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Oct 10 12:30:18 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9625e10..7c56964 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 313ea4a..2c068c4 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '11.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index c937f3d..19052d9 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -148,6 +148,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 522CFF8AA87CE84C5099DE99 /* [CP] Embed Pods Frameworks */, + E130B74B1846719F2FB4738B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -164,7 +165,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -215,6 +216,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -230,16 +232,26 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", + "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", + "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework", + "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_blue_plus/flutter_blue_plus.framework", - "${BUILT_PRODUCTS_DIR}/iOSDFULibrary/iOSDFULibrary.framework", + "${BUILT_PRODUCTS_DIR}/iOSDFULibrary/NordicDFU.framework", "${BUILT_PRODUCTS_DIR}/nordic_dfu/nordic_dfu.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZIPFoundation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_blue_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/iOSDFULibrary.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NordicDFU.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nordic_dfu.framework", ); runOnlyForDeploymentPostprocessing = 0; @@ -262,6 +274,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; + E130B74B1846719F2FB4738B /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; E2ED7C413BEA45F9D2AACCF9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -357,7 +387,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -371,14 +401,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = RCH2VG82SH; + DEVELOPMENT_TEAM = 3K8Q7WKS3W; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -439,7 +469,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -486,7 +516,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -502,14 +532,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = RCH2VG82SH; + DEVELOPMENT_TEAM = 3K8Q7WKS3W; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -535,14 +565,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = RCH2VG82SH; + DEVELOPMENT_TEAM = 3K8Q7WKS3W; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index f156018..a00db7a 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ runApp(const MyApp()); class ExampleDfuState { - bool dfuRunning = false; - int? progressPercent; - ExampleDfuState({ required this.dfuRunning, this.progressPercent, }); + bool dfuRunning = false; + int? progressPercent; } class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override - _MyAppState createState() => _MyAppState(); + MyAppState createState() => MyAppState(); } -class _MyAppState extends State { +class MyAppState extends State { StreamSubscription? scanSubscription; List scanResults = []; Map dfuStateMap = {}; - bool get anyDfuRunning => dfuStateMap.values.any((state)=>state.dfuRunning); + bool get anyDfuRunning => dfuStateMap.values.any((state) => state.dfuRunning); Future doDfu(String deviceId) async { stopScan(); - setState((){ + setState(() { dfuStateMap[deviceId] = ExampleDfuState(dfuRunning: true); }); @@ -43,13 +42,11 @@ class _MyAppState extends State { if (result == null) return; try { - final s = await NordicDfu().startDfu( - deviceId, - result.files.single.path ?? '', + final eventHandler = DfuEventHandler( onDeviceDisconnecting: (string) { debugPrint('deviceAddress: $string'); }, - // onErrorHandle: (string) { + // onError: (string) { // debugPrint('deviceAddress: $string'); // }, onProgressChanged: ( @@ -61,18 +58,25 @@ class _MyAppState extends State { partsTotal, ) { debugPrint('deviceAddress: $deviceAddress, percent: $percent'); - setState((){ + setState(() { dfuStateMap[deviceId]?.progressPercent = percent; }); }, - // androidSpecialParameter: const AndroidSpecialParameter(rebootTime: 1000), + ); + + final s = await NordicDfu().startDfu( + deviceId, + result.files.single.path ?? '', + dfuEventHandler: eventHandler, + androidParameters: const AndroidParameters(rebootTime: 1000), + // darwinParameters: const DarwinParameters(), ); debugPrint(s); - setState((){ + setState(() { dfuStateMap[deviceId]?.dfuRunning = false; }); } catch (e) { - setState((){ + setState(() { dfuStateMap[deviceId]?.dfuRunning = false; }); debugPrint(e.toString()); @@ -91,8 +95,8 @@ class _MyAppState extends State { ].request(); } - scanSubscription?.cancel(); - FlutterBluePlus.startScan(); + await scanSubscription?.cancel(); + await FlutterBluePlus.startScan(); scanResults.clear(); scanSubscription = FlutterBluePlus.scanResults.expand((e) => e).listen( (scanResult) { @@ -125,7 +129,7 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( appBar: AppBar( - title: const Text('Plugin example app'), + title: const Text('Nordic DFU Example App'), actions: [ if (anyDfuRunning) TextButton( @@ -165,8 +169,8 @@ class _MyAppState extends State { dfuState: dfuStateMap[deviceId], scanResult: result, onPress: dfuStateMap[deviceId]?.dfuRunning ?? false - ? () => NordicDfu().abortDfu(address: deviceId) - : () => doDfu(deviceId) + ? () => NordicDfu().abortDfu(address: deviceId) + : () => doDfu(deviceId), ); } } @@ -194,24 +198,24 @@ class _MyAppState extends State { // } class DeviceItem extends StatelessWidget { + const DeviceItem({ + required this.scanResult, + this.onPress, + this.dfuState, + super.key, + }); final ScanResult scanResult; final VoidCallback? onPress; final ExampleDfuState? dfuState; - const DeviceItem({ - required this.scanResult, - this.onPress, - this.dfuState, - Key? key, - }) : super(key: key); - String _getDfuButtonText() { final progressText = dfuState?.progressPercent != null - ? '\n(${dfuState!.progressPercent}%)' - : ''; - return (dfuState?.dfuRunning == true ? 'Abort Dfu' : 'Start Dfu') + progressText; + ? '\n(${dfuState!.progressPercent}%)' + : ''; + return ((dfuState?.dfuRunning ?? false) ? 'Abort Dfu' : 'Start Dfu') + + progressText; } @override @@ -222,7 +226,7 @@ class DeviceItem extends StatelessWidget { } return Card( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Row( children: [ const Icon(Icons.bluetooth), @@ -241,7 +245,7 @@ class DeviceItem extends StatelessWidget { child: Text( _getDfuButtonText(), textAlign: TextAlign.center, - ) + ), ), ], ), diff --git a/example/macos/Podfile b/example/macos/Podfile index 364e90b..f30c857 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '12.00' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -36,5 +36,8 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.14' + end end end diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 6999c05..eabf43d 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -202,7 +202,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 83073c6..549ea78 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 49bf823..dce4ec4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,22 +3,22 @@ description: Demonstrates how to use the nordic_dfu plugin. publish_to: 'none' environment: - sdk: '>=2.12.0 <4.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: - collection: ">=1.15.0" - file_picker: ">=8.0.2" + collection: ^1.19.0 + file_picker: ^8.0.2 flutter: sdk: flutter - flutter_blue_plus: ">=1.32.4 <1.40.0" + flutter_blue_plus: ^1.32.4 nordic_dfu: path: ../ - permission_handler: ">=10.4.3" + permission_handler: ^11.0.0 dev_dependencies: flutter_test: sdk: flutter - lint: ">=2.1.2" + very_good_analysis: ^6.0.0 flutter: uses-material-design: true diff --git a/ios/Classes/NordicDfuPlugin.h b/ios/Classes/NordicDfuPlugin.h deleted file mode 100644 index a4f84c7..0000000 --- a/ios/Classes/NordicDfuPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface NordicDfuPlugin : NSObject -@end diff --git a/ios/Classes/NordicDfuPlugin.m b/ios/Classes/NordicDfuPlugin.m deleted file mode 100644 index 5bb1172..0000000 --- a/ios/Classes/NordicDfuPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "NordicDfuPlugin.h" -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "nordic_dfu-Swift.h" -#endif - -@implementation NordicDfuPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftNordicDfuPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/lib/nordic_dfu.dart b/lib/nordic_dfu.dart index 75692a7..6abce3e 100644 --- a/lib/nordic_dfu.dart +++ b/lib/nordic_dfu.dart @@ -1,3 +1,6 @@ -export 'package:nordic_dfu/src/android_special_paramter.dart'; -export 'package:nordic_dfu/src/ios_special_parameter.dart'; +export 'package:nordic_dfu/src/dfu_event_handler.dart'; export 'package:nordic_dfu/src/nordic_dfu.dart'; +export 'package:nordic_dfu/src/parameters/android_parameters.dart'; +export 'package:nordic_dfu/src/parameters/android_special_parameter.dart'; +export 'package:nordic_dfu/src/parameters/darwin_parameters.dart'; +export 'package:nordic_dfu/src/parameters/ios_special_parameter.dart'; diff --git a/lib/src/dfu_event_handler.dart b/lib/src/dfu_event_handler.dart new file mode 100644 index 0000000..15b47f5 --- /dev/null +++ b/lib/src/dfu_event_handler.dart @@ -0,0 +1,149 @@ +import 'package:flutter/foundation.dart'; + +/// Callback for when DFU status has changed. +/// [address] - The device's address associated with the event. +typedef DfuCallback = void Function(String address); + +/// Callback for when a DFU error occurs. +/// [address] - The device's address associated with the error. +/// [error] - The error code. +/// [errorType] - The type of the error. +/// [message] - The error message. +typedef DfuErrorCallback = void Function( + String address, + int error, + int errorType, + String message, +); + +/// Callback for DFU progress updates. +/// [address] - The device's address associated with the progress update. +/// [percent] - The percentage of the DFU process completed. +/// [speed] - The current speed of the DFU process. +/// [avgSpeed] - The average speed of the DFU process. +/// [currentPart] - The current firmware part being uploaded. +/// [totalParts] - The total number of firmware parts to be uploaded. +typedef DfuProgressCallback = void Function( + String address, + int percent, + double speed, + double avgSpeed, + int currentPart, + int totalParts, +); + +/// A class representing event handlers for a Device Firmware Update (DFU) process. +/// +/// This class provides a set of callback functions to handle various states and events +/// during the DFU process, including device connection, progress updates, errors, and more. +class DfuEventHandler { + /// Creates an instance of [DfuEventHandler] with the required callback functions. + /// + /// All callbacks are optional, and only those relevant to your use case need to be provided. + DfuEventHandler({ + this.onDeviceConnected, + this.onDeviceConnecting, + this.onDeviceDisconnected, + this.onDeviceDisconnecting, + this.onDfuAborted, + this.onDfuCompleted, + this.onDfuProcessStarted, + this.onDfuProcessStarting, + this.onEnablingDfuMode, + this.onFirmwareValidating, + this.onFirmwareUploading, + this.onError, + this.onProgressChanged, + }); + + /// Callback triggered when the device has successfully connected. + DfuCallback? onDeviceConnected; + + /// Callback triggered when the connection process to the device is ongoing. + DfuCallback? onDeviceConnecting; + + /// Callback triggered when the device has been disconnected. + DfuCallback? onDeviceDisconnected; + + /// Callback triggered when the disconnection process from the device is ongoing. + DfuCallback? onDeviceDisconnecting; + + /// Callback triggered when the DFU process is aborted. + DfuCallback? onDfuAborted; + + /// Callback triggered when the DFU process is successfully completed. + DfuCallback? onDfuCompleted; + + /// Callback triggered when the DFU process has started. + DfuCallback? onDfuProcessStarted; + + /// Callback triggered when the DFU process is in the initial stage of starting. + DfuCallback? onDfuProcessStarting; + + /// Callback triggered when enabling DFU mode on the device. + DfuCallback? onEnablingDfuMode; + + /// Callback triggered when the firmware validation step is in progress. + DfuCallback? onFirmwareValidating; + + /// Callback triggered when the firmware validation step is in progress. + /// + /// Not available on Android + DfuCallback? onFirmwareUploading; + + /// Callback triggered when an error occurs during the DFU process. + /// + /// Provides detailed error information. + DfuErrorCallback? onError; + + /// Callback triggered to provide progress updates during the DFU process. + /// + /// Includes information such as percentage completed and current operation details. + DfuProgressCallback? onProgressChanged; + + /// Dispatches the event based on the address, key and its value. + void dispatchEvent(String key, Map? value, String address) { + switch (key) { + case 'onDeviceConnected': + onDeviceConnected?.call(address); + case 'onDeviceConnecting': + onDeviceConnecting?.call(address); + case 'onDeviceDisconnected': + onDeviceDisconnected?.call(address); + case 'onDeviceDisconnecting': + onDeviceDisconnecting?.call(address); + case 'onDfuAborted': + onDfuAborted?.call(address); + case 'onDfuCompleted': + onDfuCompleted?.call(address); + case 'onDfuProcessStarted': + onDfuProcessStarted?.call(address); + case 'onDfuProcessStarting': + onDfuProcessStarting?.call(address); + case 'onEnablingDfuMode': + onEnablingDfuMode?.call(address); + case 'onFirmwareValidating': + onFirmwareValidating?.call(address); + case 'onFirmwareUploading': + onFirmwareValidating?.call(address); + case 'onError': + onError?.call( + address, + value!['error'] as int, + value['errorType'] as int, + value['message'] as String, + ); + case 'onProgressChanged': + onProgressChanged?.call( + address, + value!['percent'] as int, + value['speed'] as double, + value['avgSpeed'] as double, + value['currentPart'] as int, + value['partsTotal'] as int, + ); + default: + debugPrint('Unknown event key: $key'); + } + } +} diff --git a/lib/src/nordic_dfu.dart b/lib/src/nordic_dfu.dart index eb66aa1..1c4894a 100644 --- a/lib/src/nordic_dfu.dart +++ b/lib/src/nordic_dfu.dart @@ -2,73 +2,15 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:nordic_dfu/src/android_special_paramter.dart'; -import 'package:nordic_dfu/src/ios_special_parameter.dart'; +import 'package:nordic_dfu/src/dfu_event_handler.dart'; +import 'package:nordic_dfu/src/parameters/android_parameters.dart'; +import 'package:nordic_dfu/src/parameters/android_special_parameter.dart'; +import 'package:nordic_dfu/src/parameters/darwin_parameters.dart'; +import 'package:nordic_dfu/src/parameters/ios_special_parameter.dart'; -/// Callback for when dfu status has changed -/// [address] Device with error -typedef DfuCallback = void Function(String address); - -/// Callback for when dfu has error -/// [address] Device with error -/// [error] Error which occurs -/// [errorType] Error type which has occured -/// [message] Message that has been thrown with error -typedef DfuErrorCallback = void Function( - String address, - int error, - int errorType, - String message, -); - -/// Callback for when the dfu progress has changed -/// [address] Device with dfu -/// [percent] Percentage dfu completed -/// [speed] Speed of the dfu proces -/// [avgSpeed] Average speed of the dfu process -/// [currentPart] Current part being uploaded -/// [totalParts] All parts that need to be uploaded -typedef DfuProgressCallback = void Function( - String address, - int percent, - double speed, - double avgSpeed, - int currentPart, - int totalParts, -); - -class DfuEventHandler { - DfuCallback? onDeviceConnected; - DfuCallback? onDeviceConnecting; - DfuCallback? onDeviceDisconnected; - DfuCallback? onDeviceDisconnecting; - DfuCallback? onDfuAborted; - DfuCallback? onDfuCompleted; - DfuCallback? onDfuProcessStarted; - DfuCallback? onDfuProcessStarting; - DfuCallback? onEnablingDfuMode; - DfuCallback? onFirmwareValidating; - DfuErrorCallback? onError; - DfuProgressCallback? onProgressChanged; - - DfuEventHandler({ - required this.onDeviceConnected, - required this.onDeviceConnecting, - required this.onDeviceDisconnected, - required this.onDeviceDisconnecting, - required this.onDfuAborted, - required this.onDfuCompleted, - required this.onDfuProcessStarted, - required this.onDfuProcessStarting, - required this.onEnablingDfuMode, - required this.onFirmwareValidating, - required this.onError, - required this.onProgressChanged, - }); -} - -/// This singleton handles the DFU process. +/// A singleton class to handle the Nordic DFU process. class NordicDfu { /// Factory for initiating the Singleton factory NordicDfu() => _singleton; @@ -76,160 +18,133 @@ class NordicDfu { NordicDfu._internal(); static final NordicDfu _singleton = NordicDfu._internal(); - static const _namespace = 'dev.steenbakker.nordic_dfu'; - static const MethodChannel _methodChannel = - MethodChannel('$_namespace/method'); - static const EventChannel _eventChannel = EventChannel('$_namespace/event'); + static const String _methodChannelName = 'dev.steenbakker.nordic_dfu/method'; + static const String _eventChannelName = 'dev.steenbakker.nordic_dfu/event'; + + static const MethodChannel _methodChannel = MethodChannel(_methodChannelName); + static const EventChannel _eventChannel = EventChannel(_eventChannelName); StreamSubscription? _events; - Map _eventHandlerMap = {}; + final Map _eventHandlerMap = {}; void _ensureEventStreamSetup() { - if (_events != null) return; // already setup + if (_events != null) return; - _events = _eventChannel.receiveBroadcastStream().listen((data) { - data as Map; - for (final key in data.keys) { - switch (key) { - case 'onDeviceConnected': - final address = data[key] as String; - _eventHandlerMap[address]?.onDeviceConnected?.call(address); - break; - case 'onDeviceConnecting': - final address = data[key] as String; - _eventHandlerMap[address]?.onDeviceConnecting?.call(address); - break; - case 'onDeviceDisconnected': - final address = data[key] as String; - _eventHandlerMap[address]?.onDeviceDisconnected?.call(address); - break; - case 'onDeviceDisconnecting': - final address = data[key] as String; - _eventHandlerMap[address]?.onDeviceDisconnecting?.call(address); - break; - case 'onDfuAborted': - final address = data[key] as String; - _eventHandlerMap[address]?.onDfuAborted?.call(address); - _eventHandlerMap.remove(address); - break; - case 'onDfuCompleted': - final address = data[key] as String; - _eventHandlerMap[address]?.onDfuCompleted?.call(address); - _eventHandlerMap.remove(address); - break; - case 'onDfuProcessStarted': - final address = data[key] as String; - _eventHandlerMap[address]?.onDfuProcessStarted?.call(address); - break; - case 'onDfuProcessStarting': - final address = data[key] as String; - _eventHandlerMap[address]?.onDfuProcessStarting?.call(address); - break; - case 'onEnablingDfuMode': - final address = data[key] as String; - _eventHandlerMap[address]?.onEnablingDfuMode?.call(address); - break; - case 'onFirmwareValidating': - final address = data[key] as String; - _eventHandlerMap[address]?.onFirmwareValidating?.call(data[key] as String); - break; - case 'onError': - final result = Map.from(data[key] as Map); - final address = result['deviceAddress'] as String; - _eventHandlerMap[address]?.onError?.call( - address, - result['error'] as int, - result['errorType'] as int, - result['message'] as String, - ); - _eventHandlerMap.remove(address); - break; - case 'onProgressChanged': - final result = Map.from(data[key] as Map); - final address = result['deviceAddress'] as String; - _eventHandlerMap[address]?.onProgressChanged?.call( - address, - result['percent'] as int, - result['speed'] as double, - result['avgSpeed'] as double, - result['currentPart'] as int, - result['partsTotal'] as int, - ); - break; - } - } - }); + _events = _eventChannel.receiveBroadcastStream().listen( + _onEvent, + onError: _onError, + ); + } + + void _onEvent(dynamic data) { + if (data is! Map) { + debugPrint('Return value is not a map but ${data.runtimeType} $data'); + return; + } + + final events = Map.from(data); + for (final entry in events.entries) { + _handleSingleEvent(entry.key, entry.value); + } } - /// Start the DFU Process. - /// Required: - /// [address] android: mac address iOS: device uuid - /// [filePath] zip file path - /// - /// Optional: - /// [name] The device name - /// [fileInAsset] if [filePath] is a asset path like 'asset/file.zip', must set this value to true, else false - /// [forceDfu] Legacy DFU only, see in nordic library, default is false - /// [numberOfPackets] The number of packets of firmware data to be received by the DFU target before sending a new Packet Receipt Notification. - /// [enableUnsafeExperimentalButtonlessServiceInSecureDfu] see in nordic library, default is false - /// [androidSpecialParameter] this parameters is only used by android lib - /// [iosSpecialParameter] this parameters is only used by ios lib - /// - /// Callbacks: - /// [onDeviceConnected] Callback for when device is connected - /// [onDeviceConnecting] Callback for when device is connecting - /// [onDeviceDisconnected] Callback for when device is disconnected - /// [onDeviceDisconnecting] Callback for when device is disconnecting - /// [onDfuAborted] Callback for dfu is Aborted - /// [onDfuCompleted] Callback for when dfu is completed - /// [onDfuProcessStarted] Callback for when dfu has been started - /// [onDfuProcessStarting] Callback for when dfu is starting - /// [onEnablingDfuMode] Callback for when dfu mode is being enabled - /// [onFirmwareValidating] Callback for when dfu is being verified - /// [onError] Callback for when dfu has error - /// [onProgressChanged] Callback for when the dfu progress has changed + void _onError(dynamic error) { + debugPrint('Error in event stream: $error'); + } + + void _handleSingleEvent(String key, dynamic value) { + if (value == null) { + debugPrint('Value is null for key: $key'); + return; + } + + final String address; + final Map? values; + + if (value is Map) { + address = value['deviceAddress'] as String; + values = Map.from(value); + } else { + address = value as String; + values = null; + } + + final handler = _eventHandlerMap[address]; + handler?.dispatchEvent(key, values, address); + } + + /// Starts the DFU process. Future startDfu( String address, String filePath, { String? name, - bool? fileInAsset, - bool? forceDfu, + bool fileInAsset = false, + bool forceDfu = false, int? numberOfPackets, - bool? enableUnsafeExperimentalButtonlessServiceInSecureDfu, - AndroidSpecialParameter androidSpecialParameter = - const AndroidSpecialParameter(), - IosSpecialParameter iosSpecialParameter = const IosSpecialParameter(), + bool enableUnsafeExperimentalButtonlessServiceInSecureDfu = false, + @Deprecated('Use androidParameters instead') + AndroidSpecialParameter? androidSpecialParameter, + @Deprecated('Use darwinParameters instead') + IosSpecialParameter? iosSpecialParameter, + AndroidParameters androidParameters = const AndroidParameters(), + DarwinParameters darwinParameters = const DarwinParameters(), + DfuEventHandler? dfuEventHandler, + @Deprecated('Use dfuEventHandler.onDeviceConnected instead') DfuCallback? onDeviceConnected, + @Deprecated('Use dfuEventHandler.onDeviceConnecting instead') DfuCallback? onDeviceConnecting, + @Deprecated('Use dfuEventHandler.onDeviceDisconnected instead') DfuCallback? onDeviceDisconnected, + @Deprecated('Use dfuEventHandler.onDeviceDisconnecting instead') DfuCallback? onDeviceDisconnecting, + @Deprecated('Use dfuEventHandler.onDfuAborted instead') DfuCallback? onDfuAborted, + @Deprecated('Use dfuEventHandler.onDfuCompleted instead') DfuCallback? onDfuCompleted, + @Deprecated('Use dfuEventHandler.onDfuProcessStarted instead') DfuCallback? onDfuProcessStarted, + @Deprecated('Use dfuEventHandler.onDfuProcessStarting instead') DfuCallback? onDfuProcessStarting, + @Deprecated('Use dfuEventHandler.onEnablingDfuMode instead') DfuCallback? onEnablingDfuMode, + @Deprecated('Use dfuEventHandler.onFirmwareValidating instead') DfuCallback? onFirmwareValidating, + @Deprecated('Use dfuEventHandler.onError instead') DfuErrorCallback? onError, + @Deprecated('Use dfuEventHandler.onProgressChanged instead') DfuProgressCallback? onProgressChanged, }) async { _eventHandlerMap[address] = DfuEventHandler( - onDeviceConnected: onDeviceConnected, - onDeviceConnecting: onDeviceConnecting, - onDeviceDisconnected: onDeviceDisconnected, - onDeviceDisconnecting: onDeviceDisconnecting, - onDfuAborted: onDfuAborted, - onDfuCompleted: onDfuCompleted, - onDfuProcessStarted: onDfuProcessStarted, - onDfuProcessStarting: onDfuProcessStarting, - onEnablingDfuMode: onEnablingDfuMode, - onFirmwareValidating: onFirmwareValidating, - onError: onError, - onProgressChanged: onProgressChanged, + onDeviceConnected: + dfuEventHandler?.onDeviceConnected ?? onDeviceConnected, + onDeviceConnecting: + dfuEventHandler?.onDeviceConnecting ?? onDeviceConnecting, + onDeviceDisconnected: + dfuEventHandler?.onDeviceDisconnected ?? onDeviceDisconnected, + onDeviceDisconnecting: + dfuEventHandler?.onDeviceDisconnecting ?? onDeviceDisconnecting, + onDfuAborted: dfuEventHandler?.onDfuAborted ?? onDfuAborted, + onDfuCompleted: dfuEventHandler?.onDfuCompleted ?? onDfuCompleted, + onDfuProcessStarted: + dfuEventHandler?.onDfuProcessStarted ?? onDfuProcessStarted, + onDfuProcessStarting: + dfuEventHandler?.onDfuProcessStarting ?? onDfuProcessStarting, + onEnablingDfuMode: + dfuEventHandler?.onEnablingDfuMode ?? onEnablingDfuMode, + onFirmwareValidating: + dfuEventHandler?.onFirmwareValidating ?? onFirmwareValidating, + onError: dfuEventHandler?.onError ?? onError, + onProgressChanged: + dfuEventHandler?.onProgressChanged ?? onProgressChanged, ); + // if (dfuEventHandler != null) { + // _eventHandlerMap[address] = dfuEventHandler; + // } + _ensureEventStreamSetup(); - return _methodChannel.invokeMethod('startDfu', { + return _methodChannel.invokeMethod('startDfu', { 'address': address, 'filePath': filePath, 'name': name, @@ -237,30 +152,29 @@ class NordicDfu { 'forceDfu': forceDfu, 'numberOfPackets': numberOfPackets, 'enableUnsafeExperimentalButtonlessServiceInSecureDfu': - enableUnsafeExperimentalButtonlessServiceInSecureDfu, - ...androidSpecialParameter.toJson(), - ...iosSpecialParameter.toJson(), + enableUnsafeExperimentalButtonlessServiceInSecureDfu, + ...(androidSpecialParameter?.toJson() ?? androidParameters.toJson()), + ...(iosSpecialParameter?.toJson() ?? darwinParameters.toJson()), }); } - /// Abort DFU while in progress. - /// - /// Optional: - /// [address] specifies the device to abort. If no [address] is provided, - /// all running DFU processes will be aborted. - /// - /// On Android, due to current limitations of the underlying Android-DFU-Library, - /// the abort command is not device-specific and will abort all active DFU processes, - /// even if a specific [address] is provided. - Future abortDfu({ - String? address - }) async { - if (kDebugMode && address != null && Platform.isAndroid) { - print("[NordicDfu:abortDfu] Warning: abortDfu will abort all DFU processes on Android"); + /// Aborts the DFU process. + Future abortDfu({String? address}) async { + if (address != null && Platform.isAndroid) { + debugPrint( + '[NordicDfu:abortDfu] Warning: aborting all DFU processes on Android', + ); } - return _methodChannel.invokeMethod('abortDfu', - address != null ? { 'address': address } : {} + return _methodChannel.invokeMethod( + 'abortDfu', + address != null ? {'address': address} : {}, ); } + + /// Disposes of the event stream subscription. + void dispose() { + _events?.cancel(); + _events = null; + } } diff --git a/lib/src/parameters/android_parameters.dart b/lib/src/parameters/android_parameters.dart new file mode 100644 index 0000000..d0c775e --- /dev/null +++ b/lib/src/parameters/android_parameters.dart @@ -0,0 +1,100 @@ +/// Android parameters for DFUServiceInitiator object. +/// See https://github.com/NordicSemiconductor/Android-DFU-Library for more information. +class AndroidParameters { + /// Constructor for AndroidSpecialParameter + const AndroidParameters({ + this.disableNotification, + this.keepBond, + this.packetReceiptNotificationsEnabled, + this.restoreBond, + this.startAsForegroundService, + this.dataDelay = 400, + this.numberOfRetries = 10, + this.rebootTime, + }); + + ///Sets whether the progress notification in the status bar should be disabled. + ///Defaults to false. + final bool? disableNotification; + + /// + /// Sets whether the DFU service should be started as a foreground service. By default it's + /// true. According to + /// + /// https://developer.android.com/about/versions/oreo/background.html + /// the background service may be killed by the system on Android Oreo after user quits the + /// application so it is recommended to keep it as a foreground service (default) at least on + /// Android Oreo+. + /// + final bool? startAsForegroundService; + + /// Sets whether the bond information should be preserver after flashing new application. + /// This feature requires DFU Bootloader version 0.6 or newer (SDK 8.0.0+). + /// Please see the {@link DfuBaseService#EXTRA_KEEP_BOND} for more information regarding + /// requirements. Remember that currently updating the Soft Device will remove the bond + /// information. + /// + /// This flag is ignored when Secure DFU Buttonless Service is used. It will keep or remove the + /// bond depending on the Buttonless service type. + /// + final bool? keepBond; + + /// Sets whether the bond should be created after the DFU is complete. + /// Please see the {@link DfuBaseService#EXTRA_RESTORE_BOND} for more information regarding + /// requirements. + /// + /// This flag is ignored when Secure DFU Buttonless Service is used. It will keep or will not + /// restore the bond depending on the Buttonless service type. + final bool? restoreBond; + + /// Enables or disables the Packet Receipt Notification (PRN) procedure. + /// + /// By default the PRNs are disabled on devices with Android Marshmallow or newer and enabled on + /// older ones. + final bool? packetReceiptNotificationsEnabled; + + /// This method sets the duration of a delay, that the service will wait before + /// sending each data object in Secure DFU. The delay will be done after a data object is created, + /// and before any data byte is sent. The default value is 0, which disables this feature. + /// + /// It has been found, that a delay of at least 300ms reduces the risk of packet lose + /// (the bootloader needs some time to prepare flash memory) on DFU bootloader from SDK 15 and 16. + /// The delay does not have to be longer than 400 ms, as according to performed tests, such delay is sufficient. + /// + /// The longer the delay, the more time DFU will take to complete + /// (delay will be repeated for each data object (4096 bytes)). However, with too small delay + /// a packet lose may occur, causing the service to enable PRN and set them to 1 making DFU process very, very slow (but reliable). + /// + /// Default: 400 + final int dataDelay; + + /// Sets the number of retries that the DFU service will use to complete DFU. The default value is 0, for backwards compatibility reason. + /// + /// If the given value is greater than 0, the service will restart itself at most max times in case of an undesired + /// disconnection during DFU operation. This attempt counter is independent from another counter, for reconnection attempts, + /// which is equal to 3. The latter one will be used when connection will fail with an error (possible packet collision or any other reason). + /// After successful connection, the reconnection counter is reset, while the retry counter is cleared after a DFU finishes with success. + /// + /// The service will not try to retry DFU in case of any other error, for instance an error sent from the target device. + /// + /// Default: 10 + final int numberOfRetries; + + /// Sets the time required by the device to reboot. The library will wait for this time before + /// scanning for the device in bootloader mode. + /// + /// rebootTime the reboot time in milliseconds, default 0. + final int? rebootTime; + + /// Converts AndroidSpecialParameter into a json object. + Map toJson() => { + 'disableNotification': disableNotification, + 'keepBond': keepBond, + 'packetReceiptNotificationsEnabled': packetReceiptNotificationsEnabled, + 'restoreBond': restoreBond, + 'startAsForegroundService': startAsForegroundService, + 'dataDelay': dataDelay, + 'numberOfRetries': numberOfRetries, + 'rebootTime': rebootTime, + }; +} diff --git a/lib/src/android_special_paramter.dart b/lib/src/parameters/android_special_parameter.dart similarity index 100% rename from lib/src/android_special_paramter.dart rename to lib/src/parameters/android_special_parameter.dart diff --git a/lib/src/parameters/darwin_parameters.dart b/lib/src/parameters/darwin_parameters.dart new file mode 100644 index 0000000..e97545c --- /dev/null +++ b/lib/src/parameters/darwin_parameters.dart @@ -0,0 +1,108 @@ +/// iOS parameters for DFUServiceInitiator object. +/// See https://github.com/NordicSemiconductor/IOS-Pods-DFU-Library for more information. +class DarwinParameters { + /// Constructor for IosSpecialParameter + const DarwinParameters({ + this.alternativeAdvertisingNameEnabled, + this.forceScanningForNewAddressInLegacyDfu, + this.connectionTimeout, + this.dataObjectPreparationDelay, + this.alternativeAdvertisingName, + this.disableResume, + this.packetReceiptNotificationParameter, + }); + + /// By default, the Legacy DFU bootloader starting from SDK 7.1, when enabled using + /// buttonless service, advertises with the same Bluetooth address as the application + /// using direct advertisement. This complies with the Bluetooth specification. + /// However, starting from iOS 13.x, iPhones and iPads use random addresses on each + /// connection and do not expect direct advertising unless bonded. This causes thiose + /// packets being missed and not reported to the library, making reconnection to the + /// bootloader and proceeding with DFU impossible. + /// A solution requires modifying either the bootloader not to use the direct advertising, + /// or the application not to share the peer data with bootloader, in which case it will + /// advertise undirectly using address +1, like it does when the switch to bootloader mode + /// is initiated with a button. After such modification, setting this flag to true will make the + /// library scan for the bootloader using `DFUPeripheralSelector`. + /// + /// Setting this flag to true without modifying the booloader behavior will break the DFU, + /// as the direct advertising packets are empty and will not pass the default + /// `DFUPeripheralSelector`. + + final bool? forceScanningForNewAddressInLegacyDfu; + + /// Connection timeout. + /// + /// When the DFU target does not connect before the time runs out, a timeout error + /// is reported. + final double? connectionTimeout; + + /// Duration of a delay, that the service will wait before sending each data object in + /// Secure DFU. The delay will be done after a data object is created, and before + /// any data byte is sent. The default value is 0, which disables this feature for the + /// second and following data objects, but the first one will be delayed by 0.4 sec. + /// + /// It has been found, that a delay of at least 0.3 sec reduces the risk of packet lose + /// (the bootloader needs some time to prepare flash memory) on DFU bootloader from + /// SDK 15, 16 and 17. The delay does not have to be longer than 0.4 sec, as according to + /// performed tests, such delay is sufficient. + /// + /// The longer the delay, the more time DFU will take to complete (delay will be repeated for + /// each data object (4096 bytes)). However, with too small delay a packet lose may occur, + /// causing the service to enable PRN and set them to 1 making DFU process very, very slow + /// (but reliable). + /// + /// The recommended delay is from 0.3 to 0.4 second if your DFU bootloader is from + /// SDK 15, 16 or 17. Older bootloaders do not need this delay. + /// + /// This variable is ignored in Legacy DFU. + final double? dataObjectPreparationDelay; + + /// In SDK 14.0.0 a new feature was added to the Buttonless DFU for non-bonded + /// devices which allows to send a unique name to the device before it is switched + /// to bootloader mode. After jump, the bootloader will advertise with this name + /// as the Complete Local Name making it easy to select proper device. In this case + /// you don't have to override the default peripheral selector. + /// + /// Read more: + /// http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v14.0.0/service_dfu.html + /// + /// Setting this flag to false you will disable this feature. iOS DFU Library will + /// not send the 0x02-len-new name command prior jumping and will rely on the DfuPeripheralSelectorDelegate just like it used to in previous SDK. + /// + /// This flag is ignored in Legacy DFU. + /// + /// **It is recommended to keep this flag set to true unless necessary.** + /// + /// For more information read: + /// https://github.com/NordicSemiconductor/IOS-nRF-Connect/issues/16 + final bool? alternativeAdvertisingNameEnabled; + + /// If `alternativeAdvertisingNameEnabled` is `true` then this specifies the + /// alternative name to use. If nil (default) then a random name is generated. + /// + /// The maximum length of the alertnative advertising name is 20 bytes. + /// Longer name will be trundated. UTF-8 characters can be cut in the middle. + final String? alternativeAdvertisingName; + + /// Disable the ability for the DFU process to resume from where it was. + final bool? disableResume; + + /// The number of packets of firmware data to be received by the DFU target before + /// sending a new Packet Receipt Notification. If this value is 0, the packet receipt + /// notification will be disabled by the DFU target. Default value is 12. + final int? packetReceiptNotificationParameter; + + /// Converts IosSpecialParameter into a json object. + Map toJson() => { + 'alternativeAdvertisingNameEnabled': alternativeAdvertisingNameEnabled, + 'forceScanningForNewAddressInLegacyDfu': + forceScanningForNewAddressInLegacyDfu, + 'connectionTimeout': connectionTimeout, + 'dataObjectPreparationDelay': dataObjectPreparationDelay, + 'alternativeAdvertisingName': alternativeAdvertisingName, + 'disableResume': disableResume, + 'packetReceiptNotificationParameter': + packetReceiptNotificationParameter, + }; +} diff --git a/lib/src/ios_special_parameter.dart b/lib/src/parameters/ios_special_parameter.dart similarity index 100% rename from lib/src/ios_special_parameter.dart rename to lib/src/parameters/ios_special_parameter.dart diff --git a/macos/Classes/NordicDfuPlugin.swift b/macos/Classes/NordicDfuPlugin.swift deleted file mode 100644 index 6d4ddda..0000000 --- a/macos/Classes/NordicDfuPlugin.swift +++ /dev/null @@ -1,190 +0,0 @@ -import Cocoa -import FlutterMacOS -import AppKit -import NordicDFU -import CoreBluetooth - -public class NordicDfuPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, DFUServiceDelegate, DFUProgressDelegate, LoggerDelegate { - - let registrar: FlutterPluginRegistrar - var sink: FlutterEventSink! - var pendingResult: FlutterResult? - var deviceAddress: String? - private var dfuController : DFUServiceController! - - public static func register(with registrar: FlutterPluginRegistrar) { - let instance = NordicDfuPlugin(registrar) - - let method = FlutterMethodChannel(name: "dev.steenbakker.nordic_dfu/method", binaryMessenger: registrar.messenger) - - let event = FlutterEventChannel(name: - "dev.steenbakker.nordic_dfu/event", binaryMessenger: registrar.messenger) - - registrar.addMethodCallDelegate(instance, channel: method) - event.setStreamHandler(instance) - } - - init(_ registrar: FlutterPluginRegistrar) { - self.registrar = registrar - super.init() - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "startDfu": initializeDfu(call, result) - case "abortDfu" : abortDfu() - default: result(FlutterMethodNotImplemented) - } - } - - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - sink = events - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - sink = nil - return nil - } - - private func abortDfu() { - _ = dfuController?.abort() - dfuController = nil - } - - private func initializeDfu(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let arguments = call.arguments as? Dictionary else { - result(FlutterError(code: "ABNORMAL_PARAMETER", message: "no parameters", details: nil)) - return - } - let name = arguments["name"] as? String - guard let address = arguments["address"] as? String, - var filePath = arguments["filePath"] as? String else { - result(FlutterError(code: "ABNORMAL_PARAMETER", message: "address and filePath are required", details: nil)) - return - } - - let forceDfu = arguments["forceDfu"] as? Bool - - let enableUnsafeExperimentalButtonlessServiceInSecureDfu = arguments["enableUnsafeExperimentalButtonlessServiceInSecureDfu"] as? Bool - - let fileInAsset = (arguments["fileInAsset"] as? Bool) ?? false - - if (fileInAsset) { - //let key = registrar.lookupKey(forAsset: filePath) - guard let pathInAsset = Bundle.main.path(forResource: filePath, ofType: nil) else { - result(FlutterError(code: "ABNORMAL_PARAMETER", message: "file in asset not found \(filePath)", details: nil)) - return - } - - filePath = pathInAsset - } - - let alternativeAdvertisingNameEnabled = arguments["alternativeAdvertisingNameEnabled"] as? Bool - - let packetReceiptNotificationParameter = arguments["packetReceiptNotificationParameter"] as? UInt16 - - startDfu(address, - name: name, - filePath: filePath, - forceDfu: forceDfu, - enableUnsafeExperimentalButtonlessServiceInSecureDfu: enableUnsafeExperimentalButtonlessServiceInSecureDfu, - alternativeAdvertisingNameEnabled: alternativeAdvertisingNameEnabled, - packetReceiptNotificationParameter: packetReceiptNotificationParameter, - result: result) - } - - private func startDfu( - _ address: String, - name: String?, - filePath: String, - forceDfu: Bool?, - enableUnsafeExperimentalButtonlessServiceInSecureDfu: Bool?, - alternativeAdvertisingNameEnabled: Bool?, - packetReceiptNotificationParameter: UInt16?, - result: @escaping FlutterResult) { - guard let uuid = UUID(uuidString: address) else { - result(FlutterError(code: "DEVICE_ADDRESS_ERROR", message: "Device address conver to uuid failed", details: "Device uuid \(address) convert to uuid failed")) - return - } - - do{ - let firmware = try DFUFirmware(urlToZipFile: URL(fileURLWithPath: filePath)) - - - - let dfuInitiator = DFUServiceInitiator(queue: nil) - .with(firmware: firmware); - if (packetReceiptNotificationParameter != nil) { - dfuInitiator.packetReceiptNotificationParameter = packetReceiptNotificationParameter! - } - dfuInitiator.delegate = self - dfuInitiator.progressDelegate = self - dfuInitiator.logger = self - - if let enableUnsafeExperimentalButtonlessServiceInSecureDfu = enableUnsafeExperimentalButtonlessServiceInSecureDfu { - dfuInitiator.enableUnsafeExperimentalButtonlessServiceInSecureDfu = enableUnsafeExperimentalButtonlessServiceInSecureDfu - } - - if let forceDfu = forceDfu { - dfuInitiator.forceDfu = forceDfu - } - - if let alternativeAdvertisingNameEnabled = alternativeAdvertisingNameEnabled { - dfuInitiator.alternativeAdvertisingNameEnabled = alternativeAdvertisingNameEnabled - } - - pendingResult = result - deviceAddress = address - - dfuController = dfuInitiator.start(targetWithIdentifier: uuid) - } - catch{ - result(FlutterError(code: "DFU_FIRMWARE_NOT_FOUND", message: "Could not dfu zip file", details: nil)) - return - } - } - - // MARK: DFUServiceDelegate - public func dfuStateDidChange(to state: DFUState) { - switch state { - case .completed: - sink?(["onDfuCompleted":deviceAddress]) - pendingResult?(deviceAddress) - pendingResult = nil - dfuController = nil - case .disconnecting: - sink?(["onDeviceDisconnecting":deviceAddress]) - case .aborted: - sink?(["onDfuAborted": deviceAddress]) - pendingResult?(FlutterError(code: "DFU_ABORTED", message: "DFU ABORTED by user", details: "device address: \(deviceAddress!)")) - pendingResult = nil - case .connecting: - sink?(["onDeviceConnecting":deviceAddress]) - case .starting: - sink?(["onDfuProcessStarting":deviceAddress]) - case .enablingDfuMode: - sink?(["onEnablingDfuMode":deviceAddress]) - case .validating: - sink?(["onFirmwareValidating":deviceAddress]) - case .uploading: - sink?(["onFirmwareUploading":deviceAddress]) - } - } - - public func dfuError(_ error: DFUError, didOccurWithMessage message: String) { - sink?(["onError":["deviceAddress": deviceAddress!, "error": error.rawValue, "errorType":error.rawValue, "message": message]]) - pendingResult?(FlutterError(code: "\(error.rawValue)", message: "DFU FAILED: \(message)", details: "Address: \(deviceAddress!), Error type \(error.rawValue)")) - pendingResult = nil - } - - //MARK: DFUProgressDelegate - public func dfuProgressDidChange(for part: Int, outOf totalParts: Int, to progress: Int, currentSpeedBytesPerSecond: Double, avgSpeedBytesPerSecond: Double) { - sink?(["onProgressChanged":["deviceAddress": deviceAddress!, "percent": progress, "speed":currentSpeedBytesPerSecond, "avgSpeed": avgSpeedBytesPerSecond, "currentPart": part, "partsTotal": totalParts]]) - } - - //MARK: - LoggerDelegate - public func logWith(_ level: LogLevel, message: String) { - //print("\(level.name()): \(message)") - } -} diff --git a/macos/nordic_dfu.podspec b/macos/nordic_dfu.podspec deleted file mode 100644 index 2dbf4b0..0000000 --- a/macos/nordic_dfu.podspec +++ /dev/null @@ -1,25 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint hello.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'nordic_dfu' - s.version = '1.0.0' - s.summary = 'MACOS DFU plugin for flutter.' - s.description = <<-DESC -A new Flutter plugin project. - DESC - s.homepage = 'http://www.timeyaa.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Timeyaa' => 'fengqiangboy@timeyaa.com' } - - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.swift_version = '5.4' - s.dependency 'FlutterMacOS' - s.dependency 'iOSDFULibrary', '~> 4.15.3' - - s.platform = :osx, '10.14' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end diff --git a/pubspec.yaml b/pubspec.yaml index b6fa9d8..f70ac9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: nordic_dfu description: This library allows you to do a Device Firmware Update (DFU) of your nrf51 or nrf52 chip from Nordic Semiconductor. Fork of flutter-nordic-dfu. -version: 6.2.1 +version: 7.0.0 homepage: https://github.com/juliansteenbakker/nordic_dfu environment: - sdk: '>=2.12.0 <4.0.0' - flutter: '>=1.10.0' + sdk: '>=3.0.0 <4.0.0' + flutter: ">=3.7.0" dependencies: flutter: @@ -22,5 +22,7 @@ flutter: pluginClass: NordicDfuPlugin ios: pluginClass: NordicDfuPlugin + sharedDarwinSource: true macos: pluginClass: NordicDfuPlugin + sharedDarwinSource: true