├── .gitignore ├── Cartfile ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Example.xcscheme ├── Example.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── baseline_developer_board_black_24dp.imageset │ │ │ ├── Contents.json │ │ │ ├── baseline_developer_board_black_24dp-1.png │ │ │ └── baseline_developer_board_black_24dp.png │ │ ├── baseline_folder_open_black_24dp.imageset │ │ │ ├── Contents.json │ │ │ ├── baseline_folder_open_black_24dp-1.png │ │ │ └── baseline_folder_open_black_24dp.png │ │ ├── baseline_get_app_black_24dp.imageset │ │ │ ├── Contents.json │ │ │ ├── baseline_get_app_black_24dp-1.png │ │ │ └── baseline_get_app_black_24dp.png │ │ ├── baseline_poll_black_24dp.imageset │ │ │ ├── Contents.json │ │ │ ├── baseline_poll_black_24dp-1.png │ │ │ └── baseline_poll_black_24dp.png │ │ ├── bubble_received.imageset │ │ │ ├── Contents.json │ │ │ ├── bubble_received.png │ │ │ ├── bubble_received@2x.png │ │ │ └── bubble_received@3x.png │ │ ├── bubble_sent.imageset │ │ │ ├── Contents.json │ │ │ ├── bubble_sent.png │ │ │ ├── bubble_sent@2x.png │ │ │ └── bubble_sent@3x.png │ │ ├── ic_bluetooth_searching_48pt.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_bluetooth_searching_48pt.png │ │ │ ├── ic_bluetooth_searching_48pt_2x.png │ │ │ └── ic_bluetooth_searching_48pt_3x.png │ │ ├── ic_download.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_download.png │ │ │ ├── ic_download@2x.png │ │ │ └── ic_download@3x.png │ │ ├── ic_recents.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_recents.png │ │ │ ├── ic_recents@2x.png │ │ │ └── ic_recents@3x.png │ │ ├── ic_send.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_send.png │ │ │ ├── ic_send@2x.png │ │ │ └── ic_send@3x.png │ │ ├── rssi_1.imageset │ │ │ ├── Contents.json │ │ │ ├── rssi_1-1.png │ │ │ ├── rssi_1-2.png │ │ │ └── rssi_1.png │ │ ├── rssi_2.imageset │ │ │ ├── Contents.json │ │ │ ├── rssi_2-1.png │ │ │ ├── rssi_2-2.png │ │ │ └── rssi_2.png │ │ ├── rssi_3.imageset │ │ │ ├── Contents.json │ │ │ ├── rssi_3-1.png │ │ │ ├── rssi_3-2.png │ │ │ └── rssi_3.png │ │ └── rssi_4.imageset │ │ │ ├── Contents.json │ │ │ ├── rssi_4-1.png │ │ │ ├── rssi_4-2.png │ │ │ └── rssi_4.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Example.entitlements │ ├── Info.plist │ ├── Model │ │ └── DiscoveredPeripheral.swift │ ├── Util │ │ ├── Data+McuManager.swift │ │ └── UIColor.swift │ └── View Controllers │ │ ├── Manager │ │ ├── BaseViewController.swift │ │ ├── DeviceController.swift │ │ ├── FileDownloadViewController.swift │ │ ├── FileUploadViewController.swift │ │ ├── FilesController.swift │ │ ├── FirmwareUpgradeViewController.swift │ │ ├── FirmwareUploadViewController.swift │ │ ├── ImageController.swift │ │ ├── LogsStatsController.swift │ │ ├── ResetViewController.swift │ │ └── Widgets │ │ │ ├── ConnectionStateLabel.swift │ │ │ ├── ImagesViewController.swift │ │ │ └── McuMgrViewController.swift │ │ ├── RootViewController.swift │ │ └── Scanner │ │ ├── ScannerFilterViewController.swift │ │ ├── ScannerTableViewCell.swift │ │ └── ScannerViewController.swift ├── Podfile └── Podfile.lock ├── LICENSE ├── McuManager.podspec ├── McuManager.xcworkspace └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Source ├── Bluetooth │ └── McuMgrBleTransport.swift ├── Extensions │ ├── CBOR+McuManager.swift │ ├── Data+McuManager.swift │ └── String+McuManager.swift ├── Info.plist ├── Managers │ ├── ConfigManager.swift │ ├── CrashManager.swift │ ├── DFU │ │ ├── FirmwareUpgradeController.swift │ │ └── FirmwareUpgradeManager.swift │ ├── DefaultManager.swift │ ├── FileSystemManager.swift │ ├── ImageManager.swift │ ├── LogManager.swift │ ├── RunTestManager.swift │ └── StatsManager.swift ├── McuManager.swift ├── McuMgrHeader.swift ├── McuMgrImage.swift ├── McuMgrLogDelegate.swift ├── McuMgrResponse.swift ├── McuMgrTransport.swift └── Utils │ ├── CBOR+String.swift │ └── ResultLock.swift └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # IntelliJ 5 | .idea/ 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | 22 | ## Other 23 | *.xccheckout 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | .build/ 38 | 39 | # Carthage 40 | Carthage 41 | Cartfile.resolved 42 | 43 | # CocoaPods 44 | Example/Pods/ 45 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "bgiori/SwiftCBOR" "v0.3.2" 2 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import os.log 9 | import McuManager 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | UserDefaults.standard.register(defaults: [ 19 | "filterByUuid" : true, 20 | "filterByRssi" : false 21 | ]) 22 | return true 23 | } 24 | 25 | func applicationWillResignActive(_ application: UIApplication) { 26 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 27 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 28 | } 29 | 30 | func applicationDidEnterBackground(_ application: UIApplication) { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | func applicationWillEnterForeground(_ application: UIApplication) { 36 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 37 | } 38 | 39 | func applicationDidBecomeActive(_ application: UIApplication) { 40 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 41 | } 42 | 43 | func applicationWillTerminate(_ application: UIApplication) { 44 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 45 | } 46 | 47 | 48 | } 49 | 50 | extension AppDelegate: McuMgrLogDelegate { 51 | 52 | public func log(_ msg: String, 53 | ofCategory category: McuMgrLogCategory, 54 | atLevel level: McuMgrLogLevel) { 55 | if #available(iOS 10.0, *) { 56 | os_log("%{public}@", log: category.log, type: level.type, msg) 57 | } else { 58 | NSLog("%@", msg) 59 | } 60 | } 61 | 62 | } 63 | 64 | extension McuMgrLogLevel { 65 | 66 | /// Mapping from Mcu log levels to system log types. 67 | @available(iOS 10.0, *) 68 | var type: OSLogType { 69 | switch self { 70 | case .debug: return .debug 71 | case .verbose: return .debug 72 | case .info: return .info 73 | case .application: return .default 74 | case .warning: return .error 75 | case .error: return .fault 76 | } 77 | } 78 | 79 | } 80 | 81 | extension McuMgrLogCategory { 82 | 83 | @available(iOS 10.0, *) 84 | var log: OSLog { 85 | return OSLog(subsystem: Bundle.main.bundleIdentifier!, category: rawValue) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_developer_board_black_24dp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "baseline_developer_board_black_24dp-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "baseline_developer_board_black_24dp.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_developer_board_black_24dp.imageset/baseline_developer_board_black_24dp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_developer_board_black_24dp.imageset/baseline_developer_board_black_24dp-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_developer_board_black_24dp.imageset/baseline_developer_board_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_developer_board_black_24dp.imageset/baseline_developer_board_black_24dp.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_folder_open_black_24dp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "baseline_folder_open_black_24dp.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "baseline_folder_open_black_24dp-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_folder_open_black_24dp.imageset/baseline_folder_open_black_24dp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_folder_open_black_24dp.imageset/baseline_folder_open_black_24dp-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_folder_open_black_24dp.imageset/baseline_folder_open_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_folder_open_black_24dp.imageset/baseline_folder_open_black_24dp.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_get_app_black_24dp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "baseline_get_app_black_24dp.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "baseline_get_app_black_24dp-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_get_app_black_24dp.imageset/baseline_get_app_black_24dp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_get_app_black_24dp.imageset/baseline_get_app_black_24dp-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_get_app_black_24dp.imageset/baseline_get_app_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_get_app_black_24dp.imageset/baseline_get_app_black_24dp.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_poll_black_24dp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "baseline_poll_black_24dp.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "baseline_poll_black_24dp-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_poll_black_24dp.imageset/baseline_poll_black_24dp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_poll_black_24dp.imageset/baseline_poll_black_24dp-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/baseline_poll_black_24dp.imageset/baseline_poll_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/baseline_poll_black_24dp.imageset/baseline_poll_black_24dp.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_received.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bubble_received.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "bubble_received@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "bubble_received@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_received.imageset/bubble_received.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/bubble_received.imageset/bubble_received.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_received.imageset/bubble_received@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/bubble_received.imageset/bubble_received@2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_received.imageset/bubble_received@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/bubble_received.imageset/bubble_received@3x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_sent.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bubble_sent.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "bubble_sent@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "bubble_sent@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_sent.imageset/bubble_sent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/bubble_sent.imageset/bubble_sent.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_sent.imageset/bubble_sent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/bubble_sent.imageset/bubble_sent@2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/bubble_sent.imageset/bubble_sent@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/bubble_sent.imageset/bubble_sent@3x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_bluetooth_searching_48pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_bluetooth_searching_48pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_bluetooth_searching_48pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_bluetooth_searching_48pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_bluetooth_searching_48pt.imageset/ic_bluetooth_searching_48pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_bluetooth_searching_48pt.imageset/ic_bluetooth_searching_48pt.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_bluetooth_searching_48pt.imageset/ic_bluetooth_searching_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_bluetooth_searching_48pt.imageset/ic_bluetooth_searching_48pt_2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_bluetooth_searching_48pt.imageset/ic_bluetooth_searching_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_bluetooth_searching_48pt.imageset/ic_bluetooth_searching_48pt_3x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_download.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_download.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_download@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_download@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_download.imageset/ic_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_download.imageset/ic_download.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_download.imageset/ic_download@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_download.imageset/ic_download@2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_download.imageset/ic_download@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_download.imageset/ic_download@3x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_recents.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_recents.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_recents@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_recents@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_recents.imageset/ic_recents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_recents.imageset/ic_recents.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_recents.imageset/ic_recents@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_recents.imageset/ic_recents@2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_recents.imageset/ic_recents@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_recents.imageset/ic_recents@3x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_send.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_send.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_send@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_send@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_send.imageset/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_send.imageset/ic_send.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_send.imageset/ic_send@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_send.imageset/ic_send@2x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/ic_send.imageset/ic_send@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/ic_send.imageset/ic_send@3x.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rssi_1-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "rssi_1-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "rssi_1.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_1.imageset/rssi_1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_1.imageset/rssi_1-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_1.imageset/rssi_1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_1.imageset/rssi_1-2.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_1.imageset/rssi_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_1.imageset/rssi_1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rssi_2-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "rssi_2-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "rssi_2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_2.imageset/rssi_2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_2.imageset/rssi_2-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_2.imageset/rssi_2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_2.imageset/rssi_2-2.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_2.imageset/rssi_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_2.imageset/rssi_2.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rssi_3-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "rssi_3-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "rssi_3.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_3.imageset/rssi_3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_3.imageset/rssi_3-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_3.imageset/rssi_3-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_3.imageset/rssi_3-2.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_3.imageset/rssi_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_3.imageset/rssi_3.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rssi_4-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "rssi_4-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "rssi_4.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_4.imageset/rssi_4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_4.imageset/rssi_4-1.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_4.imageset/rssi_4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_4.imageset/rssi_4-2.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/rssi_4.imageset/rssi_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuulLabs-OSS/mcumgr-ios/a57030560c4b63b8ffd32b602a842dfbe7768308/Example/Example/Assets.xcassets/rssi_4.imageset/rssi_4.png -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.icloud-container-identifiers 6 | 7 | iCloud.$(CFBundleIdentifier) 8 | 9 | com.apple.developer.icloud-services 10 | 11 | CloudDocuments 12 | 13 | com.apple.developer.ubiquity-container-identifiers 14 | 15 | iCloud.$(CFBundleIdentifier) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Mcu Mgr Sample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSBluetoothAlwaysUsageDescription 26 | This application connects to Bluetooth LE devices with SMP service. 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Example/Example/Model/DiscoveredPeripheral.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import CoreBluetooth 8 | 9 | class DiscoveredPeripheral: NSObject { 10 | //MARK: - Properties 11 | public private(set) var basePeripheral : CBPeripheral 12 | public private(set) var advertisedName : String 13 | public private(set) var RSSI : NSNumber = -127 14 | public private(set) var highestRSSI : NSNumber = -127 15 | public private(set) var advertisedServices : [CBUUID]? 16 | 17 | init(_ aPeripheral: CBPeripheral) { 18 | basePeripheral = aPeripheral 19 | advertisedName = "" 20 | super.init() 21 | } 22 | 23 | func update(withAdvertisementData anAdvertisementDictionary: [String : Any], andRSSI anRSSI: NSNumber) { 24 | (advertisedName, advertisedServices) = parseAdvertisementData(anAdvertisementDictionary) 25 | 26 | if anRSSI.decimalValue != 127 { 27 | RSSI = anRSSI 28 | 29 | if RSSI.decimalValue > highestRSSI.decimalValue { 30 | highestRSSI = RSSI 31 | } 32 | } 33 | } 34 | 35 | private func parseAdvertisementData(_ anAdvertisementDictionary: [String : Any]) -> (String, [CBUUID]?) { 36 | var advertisedName: String 37 | var advertisedServices: [CBUUID]? 38 | 39 | if let name = anAdvertisementDictionary[CBAdvertisementDataLocalNameKey] as? String { 40 | advertisedName = name 41 | } else { 42 | advertisedName = "N/A" 43 | } 44 | if let services = anAdvertisementDictionary[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { 45 | advertisedServices = services 46 | } else { 47 | advertisedServices = nil 48 | } 49 | 50 | return (advertisedName, advertisedServices) 51 | } 52 | 53 | //MARK: - NSObject protocols 54 | override func isEqual(_ object: Any?) -> Bool { 55 | if object is DiscoveredPeripheral { 56 | let peripheralObject = object as! DiscoveredPeripheral 57 | return peripheralObject.basePeripheral.identifier == basePeripheral.identifier 58 | } else if object is CBPeripheral { 59 | let peripheralObject = object as! CBPeripheral 60 | return peripheralObject.identifier == basePeripheral.identifier 61 | } else { 62 | return false 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/Example/Util/Data+McuManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+McuManager.swift 3 | // Example 4 | // 5 | // Created by Brian Giori on 2/20/19. 6 | // Copyright © 2019 Nordic Semiconductor ASA. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | 13 | internal struct HexEncodingOptions: OptionSet { 14 | public let rawValue: Int 15 | public static let upperCase = HexEncodingOptions(rawValue: 1 << 0) 16 | public static let space = HexEncodingOptions(rawValue: 1 << 1) 17 | public init(rawValue: Int) { 18 | self.rawValue = rawValue 19 | } 20 | } 21 | 22 | internal func hexEncodedString(options: HexEncodingOptions = []) -> String { 23 | var format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" 24 | if options.contains(.space) { 25 | format.append(" ") 26 | } 27 | return map { String(format: format, $0) }.joined() 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Example/Example/Util/UIColor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | 9 | extension UIColor { 10 | 11 | static let accent: UIColor = #colorLiteral(red: 0, green: 0.5483048558, blue: 0.8252354264, alpha: 1) 12 | 13 | static let nordic: UIColor = #colorLiteral(red: 0, green: 0.7181802392, blue: 0.8448022008, alpha: 1) 14 | 15 | static let zephyr: UIColor = #colorLiteral(red: 0.231372549, green: 0.2431372549, blue: 0.3058823529, alpha: 1) 16 | 17 | static var primary: UIColor { 18 | if #available(iOS 13.0, *) { 19 | return .label 20 | } else { 21 | return .black 22 | } 23 | } 24 | 25 | static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor { 26 | if #available(iOS 13.0, *) { 27 | return UIColor { (traitCollection) -> UIColor in 28 | return traitCollection.userInterfaceStyle == .light ? light : dark 29 | } 30 | } else { 31 | return light 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class BaseViewController: UITabBarController { 11 | 12 | var transporter: McuMgrTransport! 13 | var peripheral: DiscoveredPeripheral! { 14 | didSet { 15 | let bleTransporter = McuMgrBleTransport(peripheral.basePeripheral) 16 | bleTransporter.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 17 | transporter = bleTransporter 18 | } 19 | } 20 | 21 | override func viewDidLoad() { 22 | title = peripheral.advertisedName 23 | } 24 | 25 | override func viewWillDisappear(_ animated: Bool) { 26 | transporter?.close() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/DeviceController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class DeviceController: UITableViewController, UITextFieldDelegate { 11 | 12 | @IBOutlet weak var connectionStatus: ConnectionStateLabel! 13 | @IBOutlet weak var actionSend: UIButton! 14 | @IBOutlet weak var message: UITextField! 15 | @IBOutlet weak var messageSent: UILabel! 16 | @IBOutlet weak var messageSentBackground: UIImageView! 17 | @IBOutlet weak var messageReceived: UILabel! 18 | @IBOutlet weak var messageReceivedBackground: UIImageView! 19 | 20 | @IBAction func sendTapped(_ sender: UIButton) { 21 | message.resignFirstResponder() 22 | 23 | let text = message.text! 24 | send(message: text) 25 | } 26 | 27 | private var defaultManager: DefaultManager! 28 | 29 | override func viewDidLoad() { 30 | message.delegate = self 31 | 32 | let sentBackground = #imageLiteral(resourceName: "bubble_sent") 33 | .resizableImage(withCapInsets: UIEdgeInsets(top: 17, left: 21, bottom: 17, right: 21), 34 | resizingMode: .stretch) 35 | .withRenderingMode(.alwaysTemplate) 36 | messageSentBackground.image = sentBackground 37 | 38 | let receivedBackground = #imageLiteral(resourceName: "bubble_received") 39 | .resizableImage(withCapInsets: UIEdgeInsets(top: 17, left: 21, bottom: 17, right: 21), 40 | resizingMode: .stretch) 41 | .withRenderingMode(.alwaysTemplate) 42 | messageReceivedBackground.image = receivedBackground 43 | 44 | let baseController = parent as! BaseViewController 45 | let transporter = baseController.transporter! 46 | defaultManager = DefaultManager(transporter: transporter) 47 | defaultManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 48 | } 49 | 50 | override func viewDidAppear(_ animated: Bool) { 51 | // Set the connection status label as transport delegate. 52 | let bleTransporter = defaultManager.transporter as? McuMgrBleTransport 53 | bleTransporter?.delegate = connectionStatus 54 | } 55 | 56 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 57 | sendTapped(actionSend) 58 | return true 59 | } 60 | 61 | private func send(message: String) { 62 | messageSent.text = message 63 | messageSent.isHidden = false 64 | messageSentBackground.isHidden = false 65 | messageReceived.isHidden = true 66 | messageReceivedBackground.isHidden = true 67 | 68 | defaultManager.echo(message) { (response, error) in 69 | if let response = response { 70 | self.messageReceived.text = response.response 71 | self.messageReceivedBackground.tintColor = .zephyr 72 | } 73 | if let error = error { 74 | self.messageReceived.text = "\(error.localizedDescription)" 75 | self.messageReceivedBackground.tintColor = .systemRed 76 | } 77 | self.messageReceived.isHidden = false 78 | self.messageReceivedBackground.isHidden = false 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/FileDownloadViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class FileDownloadViewController: UIViewController, McuMgrViewController { 11 | 12 | private let recentsKey = "recents" 13 | 14 | @IBOutlet weak var file: UITextField! 15 | @IBOutlet weak var actionOpenRecents: UIButton! 16 | @IBOutlet weak var actionDownload: UIButton! 17 | @IBOutlet weak var source: UILabel! 18 | @IBOutlet weak var progress: UIProgressView! 19 | @IBOutlet weak var fileName: UILabel! 20 | @IBOutlet weak var fileContent: UILabel! 21 | 22 | @IBAction func nameChanged(_ sender: UITextField) { 23 | refreshSource() 24 | } 25 | @IBAction func openRecents(_ sender: UIButton) { 26 | let recents = (UserDefaults.standard.array(forKey: recentsKey) ?? []) as! [String] 27 | 28 | let alert = UIAlertController(title: "Recents", message: nil, preferredStyle: .actionSheet) 29 | let action: (UIAlertAction) -> Void = { action in 30 | self.file.text = action.title! 31 | self.file.becomeFirstResponder() 32 | self.refreshSource() 33 | } 34 | recents.forEach { name in 35 | alert.addAction(UIAlertAction(title: name, style: .default, handler: action)) 36 | } 37 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 38 | alert.popoverPresentationController?.sourceView = sender 39 | present(alert, animated: true) 40 | } 41 | @IBAction func download(_ sender: Any) { 42 | file.resignFirstResponder() 43 | if !file.text!.isEmpty { 44 | addRecent(file.text!) 45 | _ = fsManager.download(name: source.text!, delegate: self) 46 | } 47 | } 48 | 49 | var transporter: McuMgrTransport! { 50 | didSet { 51 | fsManager = FileSystemManager(transporter: transporter) 52 | fsManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 53 | } 54 | } 55 | var height: CGFloat = 80 56 | var tableView: UITableView! 57 | 58 | private var fsManager: FileSystemManager! 59 | private var partition: String { 60 | return UserDefaults.standard 61 | .string(forKey: FilesController.partitionKey) 62 | ?? FilesController.defaultPartition 63 | } 64 | 65 | private func refreshSource() { 66 | source.text = "/\(partition)/\(file.text!)" 67 | } 68 | 69 | override func viewDidLoad() { 70 | let recents = UserDefaults.standard.array(forKey: recentsKey) 71 | actionOpenRecents.isEnabled = recents != nil 72 | } 73 | 74 | override func viewDidAppear(_ animated: Bool) { 75 | refreshSource() 76 | } 77 | 78 | func addRecent(_ name: String) { 79 | var recents = (UserDefaults.standard.array(forKey: recentsKey) ?? []) as! [String] 80 | if !recents.contains(where: { $0 == name }) { 81 | recents.append(name) 82 | } 83 | UserDefaults.standard.set(recents, forKey: recentsKey) 84 | actionOpenRecents.isEnabled = true 85 | } 86 | } 87 | 88 | extension FileDownloadViewController: FileDownloadDelegate { 89 | 90 | func downloadProgressDidChange(bytesDownloaded: Int, fileSize: Int, timestamp: Date) { 91 | progress.progress = Float(bytesDownloaded) / Float(fileSize) 92 | } 93 | 94 | func downloadDidFail(with error: Error) { 95 | fileName.textColor = .systemRed 96 | switch error as? FileTransferError { 97 | case .mcuMgrErrorCode(.unknown): 98 | fileName.text = "File not found" 99 | default: 100 | fileName.text = "\(error.localizedDescription)" 101 | } 102 | fileContent.text = nil 103 | progress.setProgress(0, animated: true) 104 | 105 | height = 146 106 | tableView.reloadData() 107 | } 108 | 109 | func downloadDidCancel() { 110 | progress.setProgress(0, animated: true) 111 | } 112 | 113 | func download(of name: String, didFinish data: Data) { 114 | fileName.textColor = .primary 115 | fileName.text = "\(name) (\(data.count) bytes)" 116 | fileContent.text = String(data: data, encoding: .utf8) 117 | progress.setProgress(0, animated: false) 118 | 119 | let bounds = CGSize(width: fileContent.frame.width, height: CGFloat.greatestFiniteMagnitude) 120 | let rect = fileContent.sizeThatFits(bounds) 121 | height = 146 + rect.height 122 | tableView.reloadData() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/FileUploadViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class FileUploadViewController: UIViewController, McuMgrViewController { 11 | 12 | @IBOutlet weak var fileName: UILabel! 13 | @IBOutlet weak var fileSize: UILabel! 14 | @IBOutlet weak var destination: UILabel! 15 | @IBOutlet weak var status: UILabel! 16 | @IBOutlet weak var progress: UIProgressView! 17 | 18 | @IBOutlet weak var actionSelect: UIButton! 19 | @IBOutlet weak var actionStart: UIButton! 20 | @IBOutlet weak var actionPause: UIButton! 21 | @IBOutlet weak var actionResume: UIButton! 22 | @IBOutlet weak var actionCancel: UIButton! 23 | 24 | @IBAction func selectFile(_ sender: UIButton) { 25 | let importMenu = UIDocumentMenuViewController(documentTypes: ["public.data", "public.content"], in: .import) 26 | importMenu.delegate = self 27 | importMenu.popoverPresentationController?.sourceView = actionSelect 28 | present(importMenu, animated: true, completion: nil) 29 | } 30 | @IBAction func start(_ sender: UIButton) { 31 | let downloadViewController = (parent as? FilesController)?.fileDownloadViewController 32 | downloadViewController?.addRecent(fileName.text!) 33 | 34 | actionStart.isHidden = true 35 | actionPause.isHidden = false 36 | actionCancel.isHidden = false 37 | actionSelect.isEnabled = false 38 | status.textColor = .primary 39 | status.text = "UPLOADING..." 40 | _ = fsManager.upload(name: destination.text!, data: fileData!, delegate: self) 41 | } 42 | @IBAction func pause(_ sender: UIButton) { 43 | status.textColor = .primary 44 | status.text = "PAUSED" 45 | actionPause.isHidden = true 46 | actionResume.isHidden = false 47 | fsManager.pauseTransfer() 48 | } 49 | @IBAction func resume(_ sender: UIButton) { 50 | status.textColor = .primary 51 | status.text = "UPLOADING..." 52 | actionPause.isHidden = false 53 | actionResume.isHidden = true 54 | fsManager.continueTransfer() 55 | } 56 | @IBAction func cancel(_ sender: UIButton) { 57 | fsManager.cancelTransfer() 58 | } 59 | 60 | var transporter: McuMgrTransport! { 61 | didSet { 62 | fsManager = FileSystemManager(transporter: transporter) 63 | fsManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 64 | } 65 | } 66 | 67 | private var fsManager: FileSystemManager! 68 | private var fileData: Data? 69 | private var partition: String { 70 | return UserDefaults.standard 71 | .string(forKey: FilesController.partitionKey) 72 | ?? FilesController.defaultPartition 73 | } 74 | 75 | private func refreshDestination() { 76 | if let _ = fileData { 77 | destination.text = "/\(partition)/\(fileName.text!)" 78 | } 79 | } 80 | 81 | override func viewDidAppear(_ animated: Bool) { 82 | refreshDestination() 83 | } 84 | } 85 | 86 | extension FileUploadViewController: FileUploadDelegate { 87 | 88 | func uploadProgressDidChange(bytesSent: Int, fileSize: Int, timestamp: Date) { 89 | progress.setProgress(Float(bytesSent) / Float(fileSize), animated: true) 90 | } 91 | 92 | func uploadDidFail(with error: Error) { 93 | progress.setProgress(0, animated: true) 94 | actionPause.isHidden = true 95 | actionResume.isHidden = true 96 | actionCancel.isHidden = true 97 | actionStart.isHidden = false 98 | actionSelect.isEnabled = true 99 | status.textColor = .systemRed 100 | status.text = "\(error.localizedDescription)" 101 | } 102 | 103 | func uploadDidCancel() { 104 | progress.setProgress(0, animated: true) 105 | actionPause.isHidden = true 106 | actionResume.isHidden = true 107 | actionCancel.isHidden = true 108 | actionStart.isHidden = false 109 | actionSelect.isEnabled = true 110 | status.textColor = .primary 111 | status.text = "CANCELLED" 112 | } 113 | 114 | func uploadDidFinish() { 115 | progress.setProgress(0, animated: false) 116 | actionPause.isHidden = true 117 | actionResume.isHidden = true 118 | actionCancel.isHidden = true 119 | actionStart.isHidden = false 120 | actionStart.isEnabled = false 121 | actionSelect.isEnabled = true 122 | status.textColor = .primary 123 | status.text = "UPLOAD COMPLETE" 124 | fileData = nil 125 | } 126 | } 127 | 128 | // MARK: - Document Picker 129 | extension FileUploadViewController: UIDocumentMenuDelegate, UIDocumentPickerDelegate { 130 | 131 | func documentMenu(_ documentMenu: UIDocumentMenuViewController, 132 | didPickDocumentPicker documentPicker: UIDocumentPickerViewController) { 133 | documentPicker.delegate = self 134 | present(documentPicker, animated: true, completion: nil) 135 | } 136 | 137 | func documentPicker(_ controller: UIDocumentPickerViewController, 138 | didPickDocumentAt url: URL) { 139 | if let data = dataFrom(url: url) { 140 | self.fileData = data 141 | 142 | fileName.text = url.lastPathComponent 143 | fileSize.text = "\(data.count) bytes" 144 | refreshDestination() 145 | 146 | status.textColor = .primary 147 | status.text = "READY" 148 | actionStart.isEnabled = true 149 | } 150 | } 151 | 152 | /// Get the file data from the document URL 153 | private func dataFrom(url: URL) -> Data? { 154 | do { 155 | return try Data(contentsOf: url) 156 | } catch { 157 | print("Error reading file: \(error)") 158 | status.textColor = .systemRed 159 | status.text = "COULD NOT OPEN FILE" 160 | return nil 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/FilesController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class FilesController: UITableViewController { 11 | static let partitionKey = "partition" 12 | static let defaultPartition = "nffs" 13 | 14 | @IBOutlet weak var connectionStatus: ConnectionStateLabel! 15 | 16 | var fileDownloadViewController: FileDownloadViewController! 17 | 18 | override func viewDidAppear(_ animated: Bool) { 19 | showPartitionControl() 20 | 21 | // Set the connection status label as transport delegate. 22 | let baseController = parent as! BaseViewController 23 | let bleTransporter = baseController.transporter as? McuMgrBleTransport 24 | bleTransporter?.delegate = connectionStatus 25 | } 26 | 27 | override func viewDidDisappear(_ animated: Bool) { 28 | tabBarController!.navigationItem.rightBarButtonItem = nil 29 | } 30 | 31 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 32 | let baseController = parent as! BaseViewController 33 | let transporter = baseController.transporter! 34 | 35 | var destination = segue.destination as? McuMgrViewController 36 | destination?.transporter = transporter 37 | 38 | if let controller = destination as? FileDownloadViewController { 39 | fileDownloadViewController = controller 40 | fileDownloadViewController.tableView = tableView 41 | } 42 | } 43 | 44 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 45 | if indexPath.section == 2 /* Download */ { 46 | return fileDownloadViewController.height 47 | } 48 | return super.tableView(tableView, heightForRowAt: indexPath) 49 | } 50 | 51 | // MARK: Partition settings 52 | private func showPartitionControl() { 53 | let navItem = tabBarController!.navigationItem 54 | navItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, 55 | target: self, 56 | action: #selector(presentPartitionSettings)) 57 | } 58 | 59 | @objc func presentPartitionSettings() { 60 | let alert = UIAlertController(title: "Settings", 61 | message: "Specify the mount point,\ne.g. \"nffs\" or \"lfs\":", 62 | preferredStyle: .alert) 63 | alert.addTextField { field in 64 | field.placeholder = "Partition" 65 | field.autocorrectionType = .no 66 | field.autocapitalizationType = .none 67 | field.returnKeyType = .done 68 | field.clearButtonMode = .always 69 | field.text = UserDefaults.standard 70 | .string(forKey: FilesController.partitionKey) 71 | ?? FilesController.defaultPartition 72 | } 73 | alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in 74 | let newName = alert.textFields![0].text 75 | if let newName = newName, !newName.isEmpty { 76 | UserDefaults.standard.set(alert.textFields![0].text, 77 | forKey: FilesController.partitionKey) 78 | self.tableView.reloadData() 79 | } 80 | }) 81 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 82 | alert.addAction(UIAlertAction(title: "Default (\(FilesController.defaultPartition))", 83 | style: .default) { _ in 84 | UserDefaults.standard.set(FilesController.defaultPartition, 85 | forKey: FilesController.partitionKey) 86 | self.tableView.reloadData() 87 | }) 88 | present(alert, animated: true) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/FirmwareUpgradeViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class FirmwareUpgradeViewController: UIViewController, McuMgrViewController { 11 | 12 | @IBOutlet weak var actionSelect: UIButton! 13 | @IBOutlet weak var actionStart: UIButton! 14 | @IBOutlet weak var actionPause: UIButton! 15 | @IBOutlet weak var actionResume: UIButton! 16 | @IBOutlet weak var actionCancel: UIButton! 17 | @IBOutlet weak var status: UILabel! 18 | @IBOutlet weak var fileName: UILabel! 19 | @IBOutlet weak var fileSize: UILabel! 20 | @IBOutlet weak var fileHash: UILabel! 21 | @IBOutlet weak var progress: UIProgressView! 22 | 23 | @IBAction func selectFirmware(_ sender: UIButton) { 24 | let importMenu = UIDocumentMenuViewController(documentTypes: ["public.data", "public.content"], 25 | in: .import) 26 | importMenu.delegate = self 27 | importMenu.popoverPresentationController?.sourceView = actionSelect 28 | present(importMenu, animated: true, completion: nil) 29 | } 30 | @IBAction func start(_ sender: UIButton) { 31 | selectMode(for: imageData!) 32 | } 33 | @IBAction func pause(_ sender: UIButton) { 34 | dfuManager.pause() 35 | actionPause.isHidden = true 36 | actionResume.isHidden = false 37 | status.text = "PAUSED" 38 | } 39 | @IBAction func resume(_ sender: UIButton) { 40 | dfuManager.resume() 41 | actionPause.isHidden = false 42 | actionResume.isHidden = true 43 | status.text = "UPLOADING..." 44 | } 45 | @IBAction func cancel(_ sender: UIButton) { 46 | dfuManager.cancel() 47 | } 48 | 49 | private var imageData: Data? 50 | private var dfuManager: FirmwareUpgradeManager! 51 | var transporter: McuMgrTransport! { 52 | didSet { 53 | dfuManager = FirmwareUpgradeManager(transporter: transporter, delegate: self) 54 | dfuManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 55 | // nRF52840 requires ~ 10 seconds for swapping images. 56 | // Adjust this parameter for your device. 57 | dfuManager.estimatedSwapTime = 10.0 58 | } 59 | } 60 | 61 | private func selectMode(for imageData: Data) { 62 | let alertController = UIAlertController(title: "Select mode", message: nil, preferredStyle: .actionSheet) 63 | alertController.addAction(UIAlertAction(title: "Test and confirm", style: .default) { 64 | action in 65 | self.dfuManager!.mode = .testAndConfirm 66 | self.startFirmwareUpgrade(imageData: imageData) 67 | }) 68 | alertController.addAction(UIAlertAction(title: "Test only", style: .default) { 69 | action in 70 | self.dfuManager!.mode = .testOnly 71 | self.startFirmwareUpgrade(imageData: imageData) 72 | }) 73 | alertController.addAction(UIAlertAction(title: "Confirm only", style: .default) { 74 | action in 75 | self.dfuManager!.mode = .confirmOnly 76 | self.startFirmwareUpgrade(imageData: imageData) 77 | }) 78 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 79 | 80 | // If the device is an ipad set the popover presentation controller 81 | if let presenter = alertController.popoverPresentationController { 82 | presenter.sourceView = self.view 83 | presenter.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) 84 | presenter.permittedArrowDirections = [] 85 | } 86 | present(alertController, animated: true) 87 | } 88 | 89 | private func startFirmwareUpgrade(imageData: Data) { 90 | do { 91 | try dfuManager.start(data: imageData) 92 | } catch { 93 | print("Error reading hash: \(error)") 94 | status.textColor = .systemRed 95 | status.text = "ERROR" 96 | actionStart.isEnabled = false 97 | } 98 | } 99 | } 100 | 101 | // MARK: - Firmware Upgrade Delegate 102 | extension FirmwareUpgradeViewController: FirmwareUpgradeDelegate { 103 | 104 | func upgradeDidStart(controller: FirmwareUpgradeController) { 105 | actionStart.isHidden = true 106 | actionPause.isHidden = false 107 | actionCancel.isHidden = false 108 | actionSelect.isEnabled = false 109 | } 110 | 111 | func upgradeStateDidChange(from previousState: FirmwareUpgradeState, to newState: FirmwareUpgradeState) { 112 | status.textColor = .primary 113 | switch newState { 114 | case .validate: 115 | status.text = "VALIDATING..." 116 | case .upload: 117 | status.text = "UPLOADING..." 118 | case .test: 119 | status.text = "TESTING..." 120 | case .confirm: 121 | status.text = "CONFIRMING..." 122 | case .reset: 123 | status.text = "RESETTING..." 124 | case .success: 125 | status.text = "UPLOAD COMPLETE" 126 | default: 127 | status.text = "" 128 | } 129 | } 130 | 131 | func upgradeDidComplete() { 132 | progress.setProgress(0, animated: false) 133 | actionPause.isHidden = true 134 | actionResume.isHidden = true 135 | actionCancel.isHidden = true 136 | actionStart.isHidden = false 137 | actionStart.isEnabled = false 138 | actionSelect.isEnabled = true 139 | imageData = nil 140 | } 141 | 142 | func upgradeDidFail(inState state: FirmwareUpgradeState, with error: Error) { 143 | progress.setProgress(0, animated: true) 144 | actionPause.isHidden = true 145 | actionResume.isHidden = true 146 | actionCancel.isHidden = true 147 | actionStart.isHidden = false 148 | actionSelect.isEnabled = true 149 | status.textColor = .systemRed 150 | status.text = "\(error.localizedDescription)" 151 | } 152 | 153 | func upgradeDidCancel(state: FirmwareUpgradeState) { 154 | progress.setProgress(0, animated: true) 155 | actionPause.isHidden = true 156 | actionResume.isHidden = true 157 | actionCancel.isHidden = true 158 | actionStart.isHidden = false 159 | actionSelect.isEnabled = true 160 | status.textColor = .primary 161 | status.text = "CANCELLED" 162 | } 163 | 164 | func uploadProgressDidChange(bytesSent: Int, imageSize: Int, timestamp: Date) { 165 | progress.setProgress(Float(bytesSent) / Float(imageSize), animated: true) 166 | } 167 | } 168 | 169 | // MARK: - Document Picker 170 | extension FirmwareUpgradeViewController: UIDocumentMenuDelegate, UIDocumentPickerDelegate { 171 | 172 | func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) { 173 | documentPicker.delegate = self 174 | present(documentPicker, animated: true, completion: nil) 175 | } 176 | 177 | func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { 178 | if let data = dataFrom(url: url) { 179 | fileName.text = url.lastPathComponent 180 | fileSize.text = "\(data.count) bytes" 181 | 182 | do { 183 | let hash = try McuMgrImage(data: data).hash 184 | 185 | imageData = data 186 | fileHash.text = hash.hexEncodedString(options: .upperCase) 187 | status.textColor = .primary 188 | status.text = "READY" 189 | actionStart.isEnabled = true 190 | } catch { 191 | print("Error reading hash: \(error)") 192 | fileHash.text = "" 193 | status.textColor = .systemRed 194 | status.text = "INVALID FILE" 195 | actionStart.isEnabled = false 196 | } 197 | } 198 | } 199 | 200 | /// Get the image data from the document URL 201 | private func dataFrom(url: URL) -> Data? { 202 | do { 203 | return try Data(contentsOf: url) 204 | } catch { 205 | print("Error reading file: \(error)") 206 | status.textColor = .systemRed 207 | status.text = "COULD NOT OPEN FILE" 208 | return nil 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/FirmwareUploadViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class FirmwareUploadViewController: UIViewController, McuMgrViewController { 11 | 12 | @IBOutlet weak var actionSelect: UIButton! 13 | @IBOutlet weak var actionStart: UIButton! 14 | @IBOutlet weak var actionPause: UIButton! 15 | @IBOutlet weak var actionResume: UIButton! 16 | @IBOutlet weak var actionCancel: UIButton! 17 | @IBOutlet weak var status: UILabel! 18 | @IBOutlet weak var fileHash: UILabel! 19 | @IBOutlet weak var fileSize: UILabel! 20 | @IBOutlet weak var fileName: UILabel! 21 | @IBOutlet weak var progress: UIProgressView! 22 | 23 | @IBAction func selectFirmware(_ sender: UIButton) { 24 | let importMenu = UIDocumentMenuViewController(documentTypes: ["public.data", "public.content"], in: .import) 25 | importMenu.delegate = self 26 | importMenu.popoverPresentationController?.sourceView = actionSelect 27 | present(importMenu, animated: true, completion: nil) 28 | } 29 | @IBAction func start(_ sender: UIButton) { 30 | actionStart.isHidden = true 31 | actionPause.isHidden = false 32 | actionCancel.isHidden = false 33 | actionSelect.isEnabled = false 34 | status.textColor = .primary 35 | status.text = "UPLOADING..." 36 | _ = imageManager.upload(data: imageData!, delegate: self) 37 | } 38 | @IBAction func pause(_ sender: UIButton) { 39 | status.textColor = .primary 40 | status.text = "PAUSED" 41 | actionPause.isHidden = true 42 | actionResume.isHidden = false 43 | imageManager.pauseUpload() 44 | } 45 | @IBAction func resume(_ sender: UIButton) { 46 | status.textColor = .primary 47 | status.text = "UPLOADING..." 48 | actionPause.isHidden = false 49 | actionResume.isHidden = true 50 | imageManager.continueUpload() 51 | } 52 | @IBAction func cancel(_ sender: UIButton) { 53 | imageManager.cancelUpload() 54 | } 55 | 56 | private var imageData: Data? 57 | private var imageManager: ImageManager! 58 | var transporter: McuMgrTransport! { 59 | didSet { 60 | imageManager = ImageManager(transporter: transporter) 61 | imageManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 62 | } 63 | } 64 | } 65 | 66 | extension FirmwareUploadViewController: ImageUploadDelegate { 67 | 68 | func uploadProgressDidChange(bytesSent: Int, imageSize: Int, timestamp: Date) { 69 | progress.setProgress(Float(bytesSent) / Float(imageSize), animated: true) 70 | } 71 | 72 | func uploadDidFail(with error: Error) { 73 | progress.setProgress(0, animated: true) 74 | actionPause.isHidden = true 75 | actionResume.isHidden = true 76 | actionCancel.isHidden = true 77 | actionStart.isHidden = false 78 | actionSelect.isEnabled = true 79 | status.textColor = .systemRed 80 | status.text = "\(error.localizedDescription)" 81 | } 82 | 83 | func uploadDidCancel() { 84 | progress.setProgress(0, animated: true) 85 | actionPause.isHidden = true 86 | actionResume.isHidden = true 87 | actionCancel.isHidden = true 88 | actionStart.isHidden = false 89 | actionSelect.isEnabled = true 90 | status.textColor = .primary 91 | status.text = "CANCELLED" 92 | } 93 | 94 | func uploadDidFinish() { 95 | progress.setProgress(0, animated: false) 96 | actionPause.isHidden = true 97 | actionResume.isHidden = true 98 | actionCancel.isHidden = true 99 | actionStart.isHidden = false 100 | actionStart.isEnabled = false 101 | actionSelect.isEnabled = true 102 | status.textColor = .primary 103 | status.text = "UPLOAD COMPLETE" 104 | imageData = nil 105 | } 106 | } 107 | 108 | // MARK: - Document Picker 109 | extension FirmwareUploadViewController: UIDocumentMenuDelegate, UIDocumentPickerDelegate { 110 | 111 | func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) { 112 | documentPicker.delegate = self 113 | present(documentPicker, animated: true, completion: nil) 114 | } 115 | 116 | func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { 117 | if let data = dataFrom(url: url) { 118 | fileName.text = url.lastPathComponent 119 | fileSize.text = "\(data.count) bytes" 120 | 121 | do { 122 | let hash = try McuMgrImage(data: data).hash 123 | 124 | imageData = data 125 | fileHash.text = hash.hexEncodedString(options: .upperCase) 126 | status.textColor = .primary 127 | status.text = "READY" 128 | actionStart.isEnabled = true 129 | } catch { 130 | print("Error reading hash: \(error)") 131 | fileHash.text = "" 132 | status.textColor = .systemRed 133 | status.text = "INVALID FILE" 134 | actionStart.isEnabled = false 135 | } 136 | } 137 | } 138 | 139 | /// Get the image data from the document URL 140 | private func dataFrom(url: URL) -> Data? { 141 | do { 142 | return try Data(contentsOf: url) 143 | } catch { 144 | print("Error reading file: \(error)") 145 | status.textColor = .systemRed 146 | status.text = "COULD NOT OPEN FILE" 147 | return nil 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/ImageController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class ImageController: UITableViewController { 11 | @IBOutlet weak var connectionStatus: ConnectionStateLabel! 12 | 13 | /// Instance if Images View Controller, required to get its 14 | /// height when data are obtained and height changes. 15 | private var imagesViewController: ImagesViewController! 16 | 17 | override func viewDidAppear(_ animated: Bool) { 18 | showModeSwitch() 19 | 20 | // Set the connection status label as transport delegate. 21 | let baseController = parent as! BaseViewController 22 | let bleTransporter = baseController.transporter as? McuMgrBleTransport 23 | bleTransporter?.delegate = connectionStatus 24 | } 25 | 26 | override func viewWillDisappear(_ animated: Bool) { 27 | tabBarController!.navigationItem.rightBarButtonItem = nil 28 | } 29 | 30 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 31 | let baseController = parent as! BaseViewController 32 | let transporter = baseController.transporter! 33 | 34 | var destination = segue.destination as? McuMgrViewController 35 | destination?.transporter = transporter 36 | 37 | if let imagesViewController = segue.destination as? ImagesViewController { 38 | self.imagesViewController = imagesViewController 39 | imagesViewController.tableView = tableView 40 | } 41 | } 42 | 43 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 44 | if indexPath.section == 3 /* Images */ { 45 | return imagesViewController.height 46 | } 47 | return super.tableView(tableView, heightForRowAt: indexPath) 48 | } 49 | 50 | // MARK: - Handling Basic / Advanced mode 51 | private var advancedMode: Bool = false 52 | 53 | @objc func modeSwitched() { 54 | showModeSwitch(toggle: true) 55 | tableView.reloadData() 56 | } 57 | 58 | private func showModeSwitch(toggle: Bool = false) { 59 | if toggle { 60 | advancedMode = !advancedMode 61 | } 62 | let action = advancedMode ? "Basic" : "Advanced" 63 | let navItem = tabBarController!.navigationItem 64 | navItem.rightBarButtonItem = UIBarButtonItem(title: action, 65 | style: .plain, 66 | target: self, 67 | action: #selector(modeSwitched)) 68 | } 69 | 70 | override func tableView(_ tableView: UITableView, 71 | heightForHeaderInSection section: Int) -> CGFloat { 72 | if (advancedMode && section == 1) || (!advancedMode && 2...4 ~= section) { 73 | return 0.1 74 | } 75 | return super.tableView(tableView, heightForHeaderInSection: section) 76 | } 77 | 78 | override func tableView(_ tableView: UITableView, 79 | heightForFooterInSection section: Int) -> CGFloat { 80 | if (advancedMode && section == 1) || (!advancedMode && 2...4 ~= section) { 81 | return 0.1 82 | } 83 | return super.tableView(tableView, heightForFooterInSection: section) 84 | } 85 | 86 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 87 | if (advancedMode && section == 1) || (!advancedMode && 2...4 ~= section) { 88 | return 0 89 | } 90 | return super.tableView(tableView, numberOfRowsInSection: section) 91 | } 92 | 93 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 94 | if (advancedMode && section == 1) || (!advancedMode && 2...4 ~= section) { 95 | return nil 96 | } 97 | return super.tableView(tableView, titleForHeaderInSection: section) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/LogsStatsController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class LogsStatsController: UITableViewController { 11 | @IBOutlet weak var connectionStatus: ConnectionStateLabel! 12 | @IBOutlet weak var stats: UILabel! 13 | @IBOutlet weak var refreshAction: UIButton! 14 | 15 | @IBAction func refreshTapped(_ sender: UIButton) { 16 | statsManager.list { (response, error) in 17 | let bounds = CGSize(width: self.stats.frame.width, height: CGFloat.greatestFiniteMagnitude) 18 | var oldRect = self.stats.sizeThatFits(bounds) 19 | 20 | if let response = response { 21 | self.stats.text = "" 22 | self.stats.textColor = .primary 23 | 24 | // Iterate all module names. 25 | if let names = response.names, !names.isEmpty { 26 | names.forEach { module in 27 | // Request stats for each module. 28 | self.statsManager.read(module: module, callback: { (stats, error2) in 29 | // And append the received stats to the UILabel. 30 | self.stats.text! += "\(module)" 31 | if let stats = stats { 32 | if let group = stats.group { 33 | self.stats.text! += " (\(group))" 34 | } 35 | self.stats.text! += ":\n" 36 | if let fields = stats.fields { 37 | for field in fields { 38 | self.stats.text! += "• \(field.key): \(field.value)\n" 39 | } 40 | } else { 41 | self.stats.text! += "• Empty\n" 42 | } 43 | } else { 44 | self.stats.text! += "\(error2!)\n" 45 | } 46 | if module != names.last { 47 | self.stats.text! += "\n" 48 | } else { 49 | self.stats.text!.removeLast() 50 | } 51 | 52 | let newRect = self.stats.sizeThatFits(bounds) 53 | let diff = newRect.height - oldRect.height 54 | oldRect = newRect 55 | self.height += diff 56 | self.tableView.reloadData() 57 | }) 58 | } 59 | } else { 60 | self.stats.text = "No stats found." 61 | } 62 | } else { 63 | self.stats.textColor = .systemRed 64 | self.stats.text = "\(error!)" 65 | 66 | let newRect = self.stats.sizeThatFits(bounds) 67 | let diff = newRect.height - oldRect.height 68 | self.height += diff 69 | self.tableView.reloadData() 70 | } 71 | } 72 | } 73 | 74 | private var statsManager: StatsManager! 75 | private var height: CGFloat = 106 76 | 77 | override func viewDidLoad() { 78 | let baseController = parent as! BaseViewController 79 | let transporter = baseController.transporter! 80 | statsManager = StatsManager(transporter: transporter) 81 | statsManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 82 | } 83 | 84 | override func viewDidAppear(_ animated: Bool) { 85 | // Set the connection status label as transport delegate. 86 | let bleTransporter = statsManager.transporter as? McuMgrBleTransport 87 | bleTransporter?.delegate = connectionStatus 88 | } 89 | 90 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 91 | if indexPath.section == 1 /* Stats */ { 92 | return height 93 | } 94 | return super.tableView(tableView, heightForRowAt: indexPath) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/ResetViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class ResetViewController: UIViewController, McuMgrViewController { 11 | 12 | @IBOutlet weak var resetAction: UIButton! 13 | 14 | @IBAction func reset(_ sender: UIButton) { 15 | resetAction.isEnabled = false 16 | defaultManager.reset { (response, error) in 17 | self.resetAction.isEnabled = true 18 | } 19 | } 20 | 21 | private var defaultManager: DefaultManager! 22 | var transporter: McuMgrTransport! { 23 | didSet { 24 | defaultManager = DefaultManager(transporter: transporter) 25 | defaultManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/Widgets/ConnectionStateLabel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import CoreBluetooth 9 | import McuManager 10 | 11 | class ConnectionStateLabel: UILabel, PeripheralDelegate { 12 | 13 | func peripheral(_ peripheral: CBPeripheral, didChangeStateTo state: PeripheralState) { 14 | switch state { 15 | case .connected: 16 | self.text = "CONNECTED" 17 | case .connecting: 18 | self.text = "CONNECTING..." 19 | case .initializing: 20 | self.text = "INITIALIZING..." 21 | case .disconnected: 22 | self.text = "DISCONNECTED" 23 | case .disconnecting: 24 | self.text = "DISCONNECTING..." 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/Widgets/ImagesViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | class ImagesViewController: UIViewController , McuMgrViewController{ 11 | 12 | @IBOutlet weak var message: UILabel! 13 | @IBOutlet weak var readAction: UIButton! 14 | @IBOutlet weak var testAction: UIButton! 15 | @IBOutlet weak var confirmAction: UIButton! 16 | @IBOutlet weak var eraseAction: UIButton! 17 | 18 | @IBAction func read(_ sender: UIButton) { 19 | busy() 20 | imageManager.list { (response, error) in 21 | self.handle(response, error) 22 | } 23 | } 24 | @IBAction func test(_ sender: UIButton) { 25 | busy() 26 | imageManager.test(hash: imageHash!) { (response, error) in 27 | self.handle(response, error) 28 | } 29 | } 30 | @IBAction func confirm(_ sender: UIButton) { 31 | busy() 32 | imageManager.confirm(hash: imageHash!) { (response, error) in 33 | self.handle(response, error) 34 | } 35 | } 36 | @IBAction func erase(_ sender: UIButton) { 37 | busy() 38 | imageManager.erase { (response, error) in 39 | if let _ = response { 40 | self.read(sender) 41 | } else { 42 | self.readAction.isEnabled = true 43 | self.message.textColor = .systemRed 44 | self.message.text = "\(error!)" 45 | } 46 | } 47 | } 48 | 49 | private var imageHash: [UInt8]? 50 | private var imageManager: ImageManager! 51 | var transporter: McuMgrTransport! { 52 | didSet { 53 | imageManager = ImageManager(transporter: transporter) 54 | imageManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 55 | } 56 | } 57 | var height: CGFloat = 110 58 | var tableView: UITableView! 59 | 60 | private func handle(_ response: McuMgrImageStateResponse?, _ error: Error?) { 61 | let bounds = CGSize(width: message.frame.width, height: CGFloat.greatestFiniteMagnitude) 62 | let oldRect = message.sizeThatFits(bounds) 63 | 64 | if let response = response { 65 | if response.isSuccess(), let images = response.images { 66 | var info = "Split status: \(response.splitStatus ?? 0)" 67 | 68 | for (i, image) in images.enumerated() { 69 | info += "\nSlot \(i)\n" + 70 | "• Version: \(image.version!)\n" + 71 | "• Hash: \(Data(image.hash).hexEncodedString(options: .upperCase))\n" + 72 | "• Flags: " 73 | if image.bootable { 74 | info += "Bootable, " 75 | } 76 | if image.pending { 77 | info += "Pending, " 78 | } 79 | if image.confirmed { 80 | info += "Confirmed, " 81 | } 82 | if image.active { 83 | info += "Active, " 84 | } 85 | if image.permanent { 86 | info += "Permanent, " 87 | } 88 | if !image.bootable && !image.pending && !image.confirmed && !image.active && !image.permanent { 89 | info += "None" 90 | } else { 91 | info = String(info.dropLast(2)) 92 | } 93 | 94 | if !image.confirmed { 95 | imageHash = image.hash 96 | } 97 | } 98 | readAction.isEnabled = true 99 | testAction.isEnabled = images.count > 1 && !images[1].pending 100 | confirmAction.isEnabled = images.count > 1 && !images[1].permanent 101 | eraseAction.isEnabled = images.count > 1 && !images[1].confirmed 102 | 103 | message.text = info 104 | message.textColor = .primary 105 | } else { // not a success 106 | readAction.isEnabled = true 107 | message.textColor = .systemRed 108 | message.text = "Device returned error: \(response.returnCode)" 109 | } 110 | } else { // no response 111 | readAction.isEnabled = true 112 | message.textColor = .systemRed 113 | if let error = error { 114 | message.text = "\(error.localizedDescription)" 115 | } else { 116 | message.text = "Empty response" 117 | } 118 | } 119 | let newRect = message.sizeThatFits(bounds) 120 | let diff = newRect.height - oldRect.height 121 | height += diff 122 | tableView.reloadData() 123 | } 124 | 125 | private func busy() { 126 | readAction.isEnabled = false 127 | testAction.isEnabled = false 128 | confirmAction.isEnabled = false 129 | eraseAction.isEnabled = false 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Manager/Widgets/McuMgrViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import McuManager 9 | 10 | protocol McuMgrViewController { 11 | 12 | var transporter: McuMgrTransport! { get set } 13 | } 14 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/RootViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | 9 | class RootViewController: UINavigationController { 10 | 11 | override var preferredStatusBarStyle: UIStatusBarStyle { 12 | return .lightContent 13 | } 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | if #available(iOS 13.0, *) { 18 | let navBarAppearance = UINavigationBarAppearance() 19 | navBarAppearance.configureWithOpaqueBackground() 20 | navBarAppearance.backgroundColor = UIColor.dynamicColor(light: .nordic, dark: .black) 21 | navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white] 22 | navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white] 23 | navigationBar.standardAppearance = navBarAppearance 24 | navigationBar.scrollEdgeAppearance = navBarAppearance 25 | } else { 26 | // Fallback on earlier versions 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Scanner/ScannerFilterViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | 9 | protocol ScannerFilterDelegate : class { 10 | /// Called when user modifies the filter. 11 | func filterSettingsDidChange(filterByUuid: Bool, filterByRssi: Bool) 12 | } 13 | 14 | class ScannerFilterViewController: UIViewController { 15 | 16 | @IBOutlet weak var filterByUuid: UISwitch! 17 | @IBOutlet weak var filterByRssi: UISwitch! 18 | 19 | var filterByUuidEnabled: Bool! 20 | var filterByRssiEnabled: Bool! 21 | weak var delegate: ScannerFilterDelegate? 22 | 23 | @IBAction func filterValueChanged(_ sender: UISwitch) { 24 | filterByUuidEnabled = filterByUuid.isOn 25 | filterByRssiEnabled = filterByRssi.isOn 26 | 27 | delegate?.filterSettingsDidChange( 28 | filterByUuid: filterByUuidEnabled, 29 | filterByRssi: filterByRssiEnabled) 30 | } 31 | 32 | override func viewWillAppear(_ animated: Bool) { 33 | filterByUuid.isOn = filterByUuidEnabled 34 | filterByRssi.isOn = filterByRssiEnabled 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Scanner/ScannerTableViewCell.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import CoreBluetooth 9 | 10 | class ScannerTableViewCell: UITableViewCell { 11 | 12 | static let reuseIdentifier = "deviceItem" 13 | 14 | @IBOutlet weak var peripheralName: UILabel! 15 | @IBOutlet weak var peripheralRSSIIcon: UIImageView! 16 | 17 | private var lastUpdateTimestamp = Date() 18 | private var peripheral: DiscoveredPeripheral! 19 | 20 | public func setupViewWithPeripheral(_ aPeripheral: DiscoveredPeripheral) { 21 | peripheral = aPeripheral 22 | peripheralName.text = aPeripheral.advertisedName 23 | 24 | let rssi = aPeripheral.RSSI.decimalValue 25 | if rssi < -60 { 26 | peripheralRSSIIcon.image = #imageLiteral(resourceName: "rssi_2") 27 | } else if rssi < -50 { 28 | peripheralRSSIIcon.image = #imageLiteral(resourceName: "rssi_3") 29 | } else if rssi < -30 { 30 | peripheralRSSIIcon.image = #imageLiteral(resourceName: "rssi_4") 31 | } else { 32 | peripheralRSSIIcon.image = #imageLiteral(resourceName: "rssi_1") 33 | } 34 | } 35 | 36 | public func peripheralUpdatedAdvertisementData(_ aPeripheral: DiscoveredPeripheral) { 37 | if Date().timeIntervalSince(lastUpdateTimestamp) > 1.0 { 38 | lastUpdateTimestamp = Date() 39 | setupViewWithPeripheral(aPeripheral) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/Example/View Controllers/Scanner/ScannerViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Nordic Semiconductor ASA. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import UIKit 8 | import CoreBluetooth 9 | import McuManager 10 | 11 | class ScannerViewController: UITableViewController, CBCentralManagerDelegate, UIPopoverPresentationControllerDelegate, ScannerFilterDelegate { 12 | 13 | @IBOutlet weak var emptyPeripheralsView: UIView! 14 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 15 | 16 | private var centralManager: CBCentralManager! 17 | private var discoveredPeripherals = [DiscoveredPeripheral]() 18 | private var filteredPeripherals = [DiscoveredPeripheral]() 19 | 20 | private var filterByUuid: Bool! 21 | private var filterByRssi: Bool! 22 | 23 | // MARK: - UIViewController 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | centralManager = CBCentralManager() 27 | centralManager.delegate = self 28 | 29 | filterByUuid = UserDefaults.standard.bool(forKey: "filterByUuid") 30 | filterByRssi = UserDefaults.standard.bool(forKey: "filterByRssi") 31 | } 32 | 33 | override func viewWillAppear(_ animated: Bool) { 34 | super.viewWillAppear(animated) 35 | discoveredPeripherals.removeAll() 36 | tableView.reloadData() 37 | } 38 | 39 | override func viewDidAppear(_ animated: Bool) { 40 | super.viewDidDisappear(animated) 41 | 42 | if centralManager.state == .poweredOn { 43 | activityIndicator.startAnimating() 44 | centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : true]) 45 | } 46 | } 47 | 48 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 49 | super.viewWillTransition(to: size, with: coordinator) 50 | if view.subviews.contains(emptyPeripheralsView) { 51 | coordinator.animate(alongsideTransition: { (context) in 52 | let width = self.emptyPeripheralsView.frame.size.width 53 | let height = self.emptyPeripheralsView.frame.size.height 54 | if context.containerView.frame.size.height > context.containerView.frame.size.width { 55 | self.emptyPeripheralsView.frame = CGRect(x: 0, 56 | y: (context.containerView.frame.size.height / 2) - (height / 2), 57 | width: width, 58 | height: height) 59 | } else { 60 | self.emptyPeripheralsView.frame = CGRect(x: 0, 61 | y: 16, 62 | width: width, 63 | height: height) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | // MARK: - Segue control 70 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 71 | let identifier = segue.identifier! 72 | switch identifier { 73 | case "showFilter": 74 | let filterController = segue.destination as! ScannerFilterViewController 75 | filterController.popoverPresentationController?.delegate = self 76 | filterController.filterByUuidEnabled = filterByUuid 77 | filterController.filterByRssiEnabled = filterByRssi 78 | filterController.delegate = self 79 | case "connect": 80 | let controller = segue.destination as! BaseViewController 81 | controller.peripheral = (sender as! DiscoveredPeripheral) 82 | default: 83 | break 84 | } 85 | } 86 | 87 | func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { 88 | // This will force the Filter ViewController 89 | // to be displayed as a popover on iPhones. 90 | return .none 91 | } 92 | 93 | // MARK: - Filter delegate 94 | func filterSettingsDidChange(filterByUuid: Bool, filterByRssi: Bool) { 95 | self.filterByUuid = filterByUuid 96 | self.filterByRssi = filterByRssi 97 | UserDefaults.standard.set(filterByUuid, forKey: "filterByUuid") 98 | UserDefaults.standard.set(filterByRssi, forKey: "filterByRssi") 99 | 100 | filteredPeripherals.removeAll() 101 | for peripheral in discoveredPeripherals { 102 | if matchesFilters(peripheral) { 103 | filteredPeripherals.append(peripheral) 104 | } 105 | } 106 | tableView.reloadData() 107 | } 108 | 109 | // MARK: - Table view data source 110 | override func numberOfSections(in tableView: UITableView) -> Int { 111 | return 1 112 | } 113 | 114 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 115 | if filteredPeripherals.count > 0 { 116 | hideEmptyPeripheralsView() 117 | } else { 118 | showEmptyPeripheralsView() 119 | } 120 | return filteredPeripherals.count 121 | } 122 | 123 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 124 | let aCell = tableView.dequeueReusableCell(withIdentifier: ScannerTableViewCell.reuseIdentifier, for: indexPath) as! ScannerTableViewCell 125 | aCell.setupViewWithPeripheral(filteredPeripherals[indexPath.row]) 126 | return aCell 127 | } 128 | 129 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 130 | tableView.deselectRow(at: indexPath, animated: true) 131 | 132 | centralManager.stopScan() 133 | activityIndicator.stopAnimating() 134 | 135 | performSegue(withIdentifier: "connect", sender: filteredPeripherals[indexPath.row]) 136 | } 137 | 138 | // MARK: - CBCentralManagerDelegate 139 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 140 | // Find peripheral among already discovered ones, or create a new 141 | // object if it is a new one. 142 | var discoveredPeripheral = discoveredPeripherals.first(where: { $0.basePeripheral.identifier == peripheral.identifier }) 143 | if discoveredPeripheral == nil { 144 | discoveredPeripheral = DiscoveredPeripheral(peripheral) 145 | discoveredPeripherals.append(discoveredPeripheral!) 146 | } 147 | 148 | // Update the object with new values. 149 | discoveredPeripheral!.update(withAdvertisementData: advertisementData, andRSSI: RSSI) 150 | 151 | // If the device is already on the filtered list, update it. 152 | // It will be shown even if the advertising packet is no longer 153 | // matching the filter. We don't want any blinking on the device list. 154 | if let index = filteredPeripherals.firstIndex(of: discoveredPeripheral!) { 155 | // Update the cell views directly, without refreshing the 156 | // whole table. 157 | if let aCell = tableView.cellForRow(at: [0, index]) as? ScannerTableViewCell { 158 | aCell.peripheralUpdatedAdvertisementData(discoveredPeripheral!) 159 | } 160 | } else { 161 | // Check if the peripheral matches the current filters. 162 | if matchesFilters(discoveredPeripheral!) { 163 | filteredPeripherals.append(discoveredPeripheral!) 164 | tableView.reloadData() 165 | } 166 | } 167 | } 168 | 169 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 170 | if central.state != .poweredOn { 171 | print("Central is not powered on") 172 | activityIndicator.stopAnimating() 173 | } else { 174 | activityIndicator.startAnimating() 175 | centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : true]) 176 | } 177 | } 178 | 179 | // MARK: - Private helper methods 180 | 181 | /// Shows the No Peripherals view. 182 | private func showEmptyPeripheralsView() { 183 | if !view.subviews.contains(emptyPeripheralsView) { 184 | view.addSubview(emptyPeripheralsView) 185 | emptyPeripheralsView.alpha = 0 186 | emptyPeripheralsView.frame = CGRect(x: 0, 187 | y: (view.frame.height / 2) - (emptyPeripheralsView.frame.size.height / 2), 188 | width: view.frame.width, 189 | height: emptyPeripheralsView.frame.height) 190 | view.bringSubviewToFront(emptyPeripheralsView) 191 | UIView.animate(withDuration: 0.5, animations: { 192 | self.emptyPeripheralsView.alpha = 1 193 | }) 194 | } 195 | } 196 | 197 | /// Hides the No Peripherals view. This method should be 198 | /// called when a first peripheral was found. 199 | private func hideEmptyPeripheralsView() { 200 | if view.subviews.contains(emptyPeripheralsView) { 201 | UIView.animate(withDuration: 0.5, animations: { 202 | self.emptyPeripheralsView.alpha = 0 203 | }, completion: { (completed) in 204 | self.emptyPeripheralsView.removeFromSuperview() 205 | }) 206 | } 207 | } 208 | 209 | /// Returns true if the discovered peripheral matches 210 | /// current filter settings. 211 | /// 212 | /// - parameter discoveredPeripheral: A peripheral to check. 213 | /// - returns: True, if the peripheral matches the filter, 214 | /// false otherwise. 215 | private func matchesFilters(_ discoveredPeripheral: DiscoveredPeripheral) -> Bool { 216 | if filterByUuid && discoveredPeripheral.advertisedServices?.contains(McuMgrBleTransport.SMP_SERVICE) != true { 217 | return false 218 | } 219 | if filterByRssi && discoveredPeripheral.highestRSSI.decimalValue < -50 { 220 | return false 221 | } 222 | return true 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | use_frameworks! 3 | 4 | target 'Example' do 5 | pod 'McuManager', :path => '../' 6 | end 7 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - McuManager (0.10.0): 3 | - SwiftCBOR (= 0.4.3) 4 | - SwiftCBOR (0.4.3) 5 | 6 | DEPENDENCIES: 7 | - McuManager (from `../`) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - SwiftCBOR 12 | 13 | EXTERNAL SOURCES: 14 | McuManager: 15 | :path: "../" 16 | 17 | SPEC CHECKSUMS: 18 | McuManager: a734d3ef5cbf2fb6021388c79f8c7895b3281e38 19 | SwiftCBOR: c7d740966575e0fd5d971466de2b6776db3f10c3 20 | 21 | PODFILE CHECKSUM: 1753bed557e63b1f87b38c0a0b6c494113220627 22 | 23 | COCOAPODS: 1.9.1 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /McuManager.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "McuManager" 3 | s.version = "0.12.0" 4 | s.license = { :type => "Apache 2.0", :file => "LICENSE" } 5 | s.summary = "A mobile management library for devices running Apache Mynewt or Zephyr" 6 | s.homepage = "https://github.com/JuulLabs-OSS/mcumgr-ios" 7 | s.authors = { "Brian Giori" => "brian.giori@juul.com" } 8 | s.source = { :git => "https://github.com/JuulLabs-OSS/mcumgr-ios.git", :tag => "#{s.version}" } 9 | s.swift_versions = ["4.2", "5.0", "5.1", "5.2"] 10 | 11 | s.ios.deployment_target = "9.0" 12 | 13 | s.source_files = "Source/**/*.{swift, h}" 14 | s.exclude_files = "Source/*.plist" 15 | 16 | s.requires_arc = true 17 | 18 | s.dependency "SwiftCBOR", "0.4.3" 19 | end 20 | -------------------------------------------------------------------------------- /McuManager.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "McuManager", 7 | platforms: [.iOS(.v9), .macOS(.v10_13)], 8 | products: [ 9 | .library( 10 | name: "McuManager", 11 | targets: ["McuManager"] 12 | ), 13 | ], 14 | dependencies: [ 15 | .package( 16 | url: "https://github.com/unrelentingtech/SwiftCBOR.git", 17 | .branch("master") 18 | ), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "McuManager", 23 | dependencies: ["SwiftCBOR"], 24 | path: "Source", 25 | exclude:["Info.plist"] 26 | ) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This repository is deprecated and no longer maintained, please use the following forks: 4 | 5 | - iOS: [NordicSemiconductor/IOS-nRF-Connect-Device-Manager](https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-Manager) 6 | - Android: [NordicSemiconductor/Android-nRF-Connect-Device-Manager](https://github.com/NordicSemiconductor/Android-nRF-Connect-Device-Manager) 7 | 8 | --- 9 | 10 | # McuManager iOS 11 | 12 | A transport agnostic implementation of the McuManager protocol (aka Newt Manager (NMP), Simple Management Protocol (SMP)) for iOS. 13 | 14 | ## Install 15 | 16 | ### Swift Package Manager 17 | 18 | In Xcode, go to *File → Swift Packages → Add Package Dependency...* and add `https://github.com/JuulLabs-OSS/mcumgr-ios.git`. 19 | 20 | ### CocoaPods 21 | 22 | ``` 23 | pod 'McuManager', '~> 0.12.0' 24 | ``` 25 | 26 | # Introduction 27 | 28 | McuManager is an application layer protocol used to manage and monitor microcontrollers running Apache Mynewt and Zephyr. More specifically, McuManagr implements over-the-air (OTA) firmware upgrades, log and stat collection, and file-system and configuration management. 29 | 30 | ## Command Groups 31 | 32 | McuManager are organized by functionality into command groups. In _mcumgr-ios_, command groups are called managers and extend the `McuManager` class. The managers (groups) implemented in _mcumgr-ios_ are: 33 | 34 | * **`DefaultManager`**: Contains commands relevant to the OS. This includes task and memory pool statistics, device time read & write, and device reset. 35 | * **`ImageManager`**: Manage image state on the device and perform image uploads. 36 | * **`StatsManager`**: Read stats from the device. 37 | * **`ConfigManager`**: Read/Write config values on the device. 38 | * **`LogManager`**: Collect logs from the device. 39 | * **`CrashManager`**: Run crash tests on the device. 40 | * **`RunTestManager`**: Runs tests on the device. 41 | * **`FileSystemManager`**: Download/upload files from the device file system. 42 | 43 | # Firmware Upgrade 44 | 45 | Firmware upgrade is generally a four step process performed using commands from the `image` and `default` commands groups: `upload`, `test`, `reset`, and `confirm`. 46 | 47 | This library provides a `FirmwareUpgradeManager` as a convinience for upgrading the image running on a device. 48 | 49 | ## FirmwareUpgradeManager 50 | 51 | A `FirmwareUpgradeManager` provides an easy way to perform firmware upgrades on a device. A `FirmwareUpgradeManager` must be initialized with an `McuMgrTransport` which defines the transport scheme and device. Once initialized, a `FirmwareUpgradeManager` can perform one firmware upgrade at a time. Firmware upgrades are started using the `start(data: Data)` method and can be paused, resumed, and canceled using `pause()`, `resume()`, and `cancel()` respectively. 52 | 53 | ### Example 54 | ```swift 55 | // Initialize the BLE transporter using a scanned peripheral 56 | let bleTransport = McuMgrBleTransport(cbPeripheral) 57 | 58 | // Initialize the FirmwareUpgradeManager using the transport and a delegate 59 | let dfuManager = FirmwareUpgradeManager(bleTransport, delegate) 60 | 61 | // Start the firmware upgrade with the image data 62 | dfuManager.start(data: imageData) 63 | ``` 64 | 65 | ### Firmware Upgrade Mode 66 | 67 | McuManager firmware upgrades can actually be performed in few different ways. These different upgrade modes determine the commands sent after the `upload` step. The `FirmwareUpgradeManager` can be configured to perform these upgrade variations by setting the `mode` property. The different firmware upgrade modes are as follows: 68 | 69 | * **`.testAndConfirm`**: This mode is the **default and recommended mode** for performing upgrades due to it's ability to recover from a bad firmware upgrade. The process for this mode is `upload`, `test`, `reset`, `confirm`. 70 | * **`.confirmOnly`**: This mode is **not recommended**. If the device fails to boot into the new image, it will not be able to recover and will need to be re-flashed. The process for this mode is `upload`, `confirm`, `reset`. 71 | * **`.testOnly`**: This mode is useful if you want to run tests on the new image running before confirming it manually as the primary boot image. The process for this mode is `upload`, `test`, `reset`. 72 | 73 | ### Firmware Upgrade State 74 | 75 | `FirmwareUpgradeManager` acts as a simple, mostly linear state machine which is determined by the `mode`. As the manager moves through the firmware upgrade process, state changes are provided through the `FirmwareUpgradeDelegate`'s `upgradeStateDidChange` method. 76 | 77 | The `FirmwareUpgradeManager` contains an additional state, `validate`, which precedes the upload. The `validate` state checks the current image state of the device in an attempt to bypass certain states of the firmware upgrade. For example, if the image to upgrade to already exists in slot 1 on the device, the `FirmwareUpgradeManager` will skip `upload` and move directly to `test` (or `confirm` if `.confirmOnly` mode has been set) from `validate`. If the uploaded image is already active, and confirmed in slot 0, the upgrade will succeed immediately. In short, the `validate` state makes it easy to reattempt an upgrade without needing to re-upload the image or manually determine where to start. 78 | 79 | # Logging 80 | 81 | Setting `logDelegate` property in a manager gives access to low level logs, that can help debugging both the app and your device. Messages are logged on 6 log levels, from `.debug` to `.error`, and additionally contain a `McuMgrLogCategory`, which identifies the originating component. Additionally, the `logDelegate` property of `McuMgrBleTransport` provides access to the BLE Transport logs. 82 | 83 | ### Example 84 | ```swift 85 | // Initialize the BLE transporter using a scanned peripheral 86 | let bleTransport = McuMgrBleTransport(cbPeripheral) 87 | bleTransporter.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 88 | 89 | // Initialize the DeviceManager using the transport and a delegate 90 | let deviceManager = DeviceManager(bleTransport, delegate) 91 | deviceManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate 92 | 93 | // Send acho 94 | deviceManger.echo("Hello World!", callback) 95 | ``` 96 | 97 | ### OSLog integration 98 | 99 | `McuMgrLogDelegate` can be easily integrated with [unified logging system](https://developer.apple.com/documentation/os/logging). An example is provided in the example app in the `AppDelegate.swift`. A `McuMgrLogLevel` extension that can be found in that file translates the log level to one of `OSLogType` levels. Similarly, `McuMgrLogCategory` extension converts the category to `OSLog` type. 100 | 101 | # Developing for McuManager 102 | 103 | Clone the repository, install pods. 104 | 105 | ``` 106 | git clone https://github.com/JuulLabs-OSS/mcumgr-ios.git 107 | cd mcumgr-ios/Example 108 | pod install 109 | ``` 110 | 111 | In Xcode (or other IDE) open the `mcumgr-ios/Example/Example.xcworkspace`. The development pod for McuManager should be under `Pods -> Development Pods -> McuManager`. 112 | 113 | -------------------------------------------------------------------------------- /Source/Extensions/CBOR+McuManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import SwiftCBOR 9 | 10 | internal extension CBOR { 11 | 12 | private func wrapQuotes(_ string: String) -> String { 13 | return "\"\(string)\"" 14 | } 15 | 16 | var description: String { 17 | switch self { 18 | case let .unsignedInt(l): return l.description 19 | case let .negativeInt(l): return l.description 20 | case let .byteString(l): return wrapQuotes(Data(l).base64EncodedString()) 21 | case let .utf8String(l): return wrapQuotes(l) 22 | case let .array(l): return l.description 23 | case let .map(l): return l.description.replaceFirst(of: "[", with: "{").replaceLast(of: "]", with: "}") 24 | case let .tagged(_, l): return l.description // TODO what to do with tags 25 | case let .simple(l): return l.description 26 | case let .boolean(l): return l.description 27 | case .null: return "null" 28 | case .undefined: return "null" 29 | case let .half(l): return l.description 30 | case let .float(l): return l.description 31 | case let .double(l): return l.description 32 | case .break: return "" 33 | case let .date(l): return l.description 34 | } 35 | } 36 | 37 | var value : Any? { 38 | switch self { 39 | case let .unsignedInt(l): return Int(l) 40 | case let .negativeInt(l): return Int(l) * -1 41 | case let .byteString(l): return l 42 | case let .utf8String(l): return l 43 | case let .array(l): return l 44 | case let .map(l): return l 45 | case let .tagged(t, l): return (t, l) 46 | case let .simple(l): return l 47 | case let .boolean(l): return l 48 | case .null: return nil 49 | case .undefined: return nil 50 | case let .half(l): return l 51 | case let .float(l): return l 52 | case let .double(l): return l 53 | case .break: return nil 54 | case let .date(l): return l 55 | } 56 | } 57 | 58 | static func toObjectMap(map: [CBOR:CBOR]?) throws -> [String:V]? { 59 | guard let map = map else { 60 | return nil 61 | } 62 | var objMap = [String:V]() 63 | for (key, value) in map { 64 | if case let CBOR.utf8String(keyString) = key { 65 | let v = try V(cbor: value) 66 | objMap.updateValue(v, forKey: keyString) 67 | } 68 | } 69 | return objMap 70 | } 71 | 72 | static func toMap(map: [CBOR:CBOR]?) throws -> [String:V]? { 73 | guard let map = map else { 74 | return nil 75 | } 76 | var objMap = [String:V]() 77 | for (key, value) in map { 78 | if case let CBOR.utf8String(keyString) = key { 79 | if let v = value.value as? V { 80 | objMap.updateValue(v, forKey: keyString) 81 | } 82 | } 83 | } 84 | return objMap 85 | } 86 | 87 | static func toObjectArray(array: [CBOR]?) throws -> [V]? { 88 | guard let array = array else { 89 | return nil 90 | } 91 | var objArray = [V]() 92 | for cbor in array { 93 | let obj = try V(cbor: cbor) 94 | objArray.append(obj) 95 | } 96 | return objArray 97 | } 98 | 99 | static func toArray(array: [CBOR]?) throws -> [V]? { 100 | guard let array = array else { 101 | return nil 102 | } 103 | var objArray = [V]() 104 | for cbor in array { 105 | if let v = cbor.value as? V { 106 | objArray.append(v) 107 | } 108 | } 109 | return objArray 110 | } 111 | } 112 | 113 | //********************************************************************************************* 114 | // MARK: CBORMappable 115 | //********************************************************************************************* 116 | 117 | open class CBORMappable { 118 | required public init(cbor: CBOR?) throws { 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Source/Extensions/Data+McuManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import CommonCrypto 9 | 10 | internal extension Data { 11 | 12 | // MARK: - Convert data to and from types 13 | 14 | init(from value: T) { 15 | var value = value 16 | self = withUnsafePointer(to: &value) { (pointer) -> Data in 17 | Data(buffer: UnsafeBufferPointer(start: pointer, count: 1)) 18 | } 19 | } 20 | 21 | func read(offset: Int = 0) -> T { 22 | let length = MemoryLayout.size 23 | 24 | #if swift(>=5.0) 25 | return subdata(in: offset ..< offset + length).withUnsafeBytes { $0.load(as: T.self) } 26 | #else 27 | return subdata(in: offset ..< offset + length).withUnsafeBytes { $0.pointee } 28 | #endif 29 | } 30 | 31 | func readBigEndian(offset: Int = 0) -> R { 32 | let r: R = read(offset: offset) 33 | return r.bigEndian 34 | } 35 | 36 | // MARK: - Hex Encoding 37 | 38 | struct HexEncodingOptions: OptionSet { 39 | public let rawValue: Int 40 | public static let upperCase = HexEncodingOptions(rawValue: 1 << 0) 41 | public static let space = HexEncodingOptions(rawValue: 1 << 1) 42 | public static let prepend0x = HexEncodingOptions(rawValue: 1 << 2) 43 | public init(rawValue: Int) { 44 | self.rawValue = rawValue 45 | } 46 | } 47 | 48 | func hexEncodedString(options: HexEncodingOptions = []) -> String { 49 | if isEmpty { 50 | return "0 bytes" 51 | } 52 | var format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" 53 | if options.contains(.space) { 54 | format.append(" ") 55 | } 56 | let prefix = options.contains(.prepend0x) ? "0x" : "" 57 | return prefix + map { String(format: format, $0) }.joined() 58 | } 59 | 60 | // MARK: - Fragmentation 61 | 62 | func fragment(size: Int) -> [Data] { 63 | return stride(from: 0, to: self.count, by: size).map { 64 | Data(self[$0.. [UInt8] { 71 | var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) 72 | withUnsafeBytes { 73 | _ = CC_SHA256($0.baseAddress, CC_LONG(count), &hash) 74 | } 75 | return hash 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /Source/Extensions/String+McuManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension String { 10 | 11 | func replaceFirst(of pattern:String, with replacement:String) -> String { 12 | if let range = self.range(of: pattern) { 13 | return self.replacingCharacters(in: range, with: replacement) 14 | } else { 15 | return self 16 | } 17 | } 18 | 19 | func replaceLast(of pattern:String, with replacement:String) -> String { 20 | if let range = self.range(of: pattern, options: String.CompareOptions.backwards) { 21 | return self.replacingCharacters(in: range, with: replacement) 22 | } else { 23 | return self 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/Managers/ConfigManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import SwiftCBOR 9 | 10 | public class ConfigManager: McuManager { 11 | override class var TAG: McuMgrLogCategory { .config } 12 | 13 | //************************************************************************** 14 | // MARK: Constants 15 | //************************************************************************** 16 | 17 | // Mcu Config Manager ids. 18 | let ID_CONFIG = UInt8(0) 19 | 20 | //************************************************************************** 21 | // MARK: Initializers 22 | //************************************************************************** 23 | 24 | public init(transporter: McuMgrTransport) { 25 | super.init(group: McuMgrGroup.config, transporter: transporter) 26 | } 27 | 28 | //************************************************************************** 29 | // MARK: Commands 30 | //************************************************************************** 31 | 32 | /// Read a system configuration variable from a device. 33 | /// 34 | /// - parameter name: The name of the system configuration variable to read. 35 | /// - parameter callback: The response callback. 36 | public func read(name: String, callback: @escaping McuMgrCallback) { 37 | let payload: [String:CBOR] = ["name": CBOR.utf8String(name)] 38 | send(op: .read, commandId: ID_CONFIG, payload: payload, callback: callback) 39 | } 40 | 41 | /// Write a system configuration variable on a device. 42 | /// 43 | /// - parameter name: The name of the sys config variable to write. 44 | /// - parameter value: The value of the sys config variable to write. 45 | /// - parameter callback: The response callback. 46 | public func write(name: String, value: String, callback: @escaping McuMgrCallback) { 47 | let payload: [String:CBOR] = ["name": CBOR.utf8String(name), 48 | "val": CBOR.utf8String(value)] 49 | send(op: .write, commandId: ID_CONFIG, payload: payload, callback: callback) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Source/Managers/CrashManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import SwiftCBOR 9 | 10 | public class CrashManager: McuManager { 11 | override class var TAG: McuMgrLogCategory { .crash } 12 | 13 | //************************************************************************** 14 | // MARK: Constants 15 | //************************************************************************** 16 | 17 | // Mcu Crash Manager ids. 18 | let ID_TEST = UInt8(0) 19 | 20 | public enum CrashTest: String { 21 | case div0 = "div0" 22 | case jump0 = "jump0" 23 | case ref0 = "ref0" 24 | case assert = "assert" 25 | case wdog = "wdog" 26 | } 27 | 28 | //************************************************************************** 29 | // MARK: Initializers 30 | //************************************************************************** 31 | 32 | public init(transporter: McuMgrTransport) { 33 | super.init(group: McuMgrGroup.crash, transporter: transporter) 34 | } 35 | 36 | //************************************************************************** 37 | // MARK: Commands 38 | //************************************************************************** 39 | 40 | /// Run a crash test on a device. 41 | /// 42 | /// - parameter crash: The crash test to run. 43 | /// - parameter callback: The response callback. 44 | public func test(crash: CrashTest, callback: @escaping McuMgrCallback) { 45 | let payload: [String:CBOR] = ["t": CBOR.utf8String(crash.rawValue)] 46 | send(op: .write, commandId: ID_TEST, payload: payload, callback: callback) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Source/Managers/DFU/FirmwareUpgradeController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirmwareUpgradeController.swift 3 | // McuManager 4 | // 5 | // Created by Aleksander Nowakowski on 05/07/2018. 6 | // Copyright © 2018 Runtime. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol FirmwareUpgradeController { 12 | 13 | /// Pause the firmware upgrade. 14 | func pause() 15 | 16 | /// Resume a paused firmware upgrade. 17 | func resume() 18 | 19 | /// Cancel the firmware upgrade. 20 | func cancel() 21 | 22 | /// Returns true if the upload has been paused. 23 | func isPaused() -> Bool 24 | 25 | /// Returns true if the upload is in progress. 26 | func isInProgress() -> Bool 27 | } 28 | -------------------------------------------------------------------------------- /Source/Managers/DefaultManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import SwiftCBOR 9 | 10 | public class DefaultManager: McuManager { 11 | override class var TAG: McuMgrLogCategory { .default } 12 | 13 | //************************************************************************** 14 | // MARK: Constants 15 | //************************************************************************** 16 | 17 | // Mcu Default Manager ids. 18 | let ID_ECHO = UInt8(0) 19 | let ID_CONS_ECHO_CTRL = UInt8(1) 20 | let ID_TASKSTATS = UInt8(2) 21 | let ID_MPSTAT = UInt8(3) 22 | let ID_DATETIME_STR = UInt8(4) 23 | let ID_RESET = UInt8(5) 24 | 25 | //************************************************************************** 26 | // MARK: Initializers 27 | //************************************************************************** 28 | 29 | public init(transporter: McuMgrTransport) { 30 | super.init(group: McuMgrGroup.default, transporter: transporter) 31 | } 32 | 33 | //************************************************************************** 34 | // MARK: Commands 35 | //************************************************************************** 36 | 37 | /// Echo a string to the device. 38 | /// 39 | /// Used primarily to test Mcu Manager. 40 | /// 41 | /// - parameter echo: The string which the device will echo. 42 | /// - parameter callback: The response callback. 43 | public func echo(_ echo: String, callback: @escaping McuMgrCallback) { 44 | let payload: [String:CBOR] = ["d": CBOR.utf8String(echo)] 45 | send(op: .write, commandId: ID_ECHO, payload: payload, callback: callback) 46 | } 47 | 48 | /// Set console echoing on the device. 49 | /// 50 | /// - parameter echoOn: Value to set console echo to. 51 | /// - parameter callback: The response callback. 52 | public func consoleEcho(_ echoOn: Bool, callback: @escaping McuMgrCallback) { 53 | let payload: [String:CBOR] = ["echo": CBOR.init(integerLiteral: echoOn ? 1 : 0)] 54 | send(op: .write, commandId: ID_CONS_ECHO_CTRL, payload: payload, callback: callback) 55 | } 56 | 57 | /// Read the task statistics for the device. 58 | /// 59 | /// - parameter callback: The response callback. 60 | public func taskStats(callback: @escaping McuMgrCallback) { 61 | send(op: .read, commandId: ID_TASKSTATS, payload: nil, callback: callback) 62 | } 63 | 64 | /// Read the memory pool statistics for the device. 65 | /// 66 | /// - parameter callback: The response callback. 67 | public func memoryPoolStats(callback: @escaping McuMgrCallback) { 68 | send(op: .read, commandId: ID_MPSTAT, payload: nil, callback: callback) 69 | } 70 | 71 | /// Read the date and time on the device. 72 | /// 73 | /// - parameter callback: The response callback. 74 | public func readDatetime(callback: @escaping McuMgrCallback) { 75 | send(op: .read, commandId: ID_DATETIME_STR, payload: nil, callback: callback) 76 | } 77 | 78 | /// Set the date and time on the device. 79 | /// 80 | /// - parameter date: The date and time to set the device's clock to. If 81 | /// this parameter is left out, the device will be set to the current date 82 | /// and time. 83 | /// - parameter timeZone: The time zone for the given date. If left out, the 84 | /// timezone will be set to the iOS system time zone. 85 | /// - parameter callback: The response callback. 86 | public func writeDatetime(date: Date = Date(), timeZone: TimeZone? = nil, 87 | callback: @escaping McuMgrCallback) { 88 | let payload: [String:CBOR] = 89 | ["datetime": CBOR.utf8String(McuManager.dateToString(date: date, timeZone: timeZone))] 90 | send(op: .write, commandId: ID_DATETIME_STR, payload: payload, callback: callback) 91 | } 92 | 93 | /// Trigger the device to soft reset. 94 | /// 95 | /// - parameter callback: The response callback. 96 | public func reset(callback: @escaping McuMgrCallback) { 97 | send(op: .write, commandId: ID_RESET, payload: nil, callback: callback) 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /Source/Managers/ImageManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import CoreBluetooth 9 | import SwiftCBOR 10 | 11 | public class ImageManager: McuManager { 12 | override class var TAG: McuMgrLogCategory { .image } 13 | 14 | private static let truncatedHashLen = 3 15 | 16 | //************************************************************************** 17 | // MARK: Constants 18 | //************************************************************************** 19 | 20 | // Mcu Image Manager command IDs. 21 | let ID_STATE = UInt8(0) 22 | let ID_UPLOAD = UInt8(1) 23 | let ID_FILE = UInt8(2) 24 | let ID_CORELIST = UInt8(3) 25 | let ID_CORELOAD = UInt8(4) 26 | let ID_ERASE = UInt8(5) 27 | let ID_ERASE_STATE = UInt8(6) 28 | 29 | //************************************************************************** 30 | // MARK: Initializers 31 | //************************************************************************** 32 | 33 | public init(transporter: McuMgrTransport) { 34 | super.init(group: McuMgrGroup.image, transporter: transporter) 35 | } 36 | 37 | //************************************************************************** 38 | // MARK: Commands 39 | //************************************************************************** 40 | 41 | /// List the images on the device. 42 | /// 43 | /// - parameter callback: The response callback. 44 | public func list(callback: @escaping McuMgrCallback) { 45 | send(op: .read, commandId: ID_STATE, payload: nil, callback: callback) 46 | } 47 | 48 | /// Sends the next packet of data from given offset. 49 | /// To send a complete image, use upload(data:image:delegate) method instead. 50 | /// 51 | /// - parameter data: The image data. 52 | /// - parameter image: The image number / slot number for DFU. 53 | /// - parameter offset: The offset from this data will be sent. 54 | /// - parameter callback: The callback. 55 | public func upload(data: Data, image: Int, offset: UInt, callback: @escaping McuMgrCallback) { 56 | // Calculate the number of remaining bytes. 57 | let remainingBytes: UInt = UInt(data.count) - offset 58 | 59 | // Data length to end is the minimum of the max data lenght and the 60 | // number of remaining bytes. 61 | let packetOverhead = calculatePacketOverhead(data: data, offset: UInt64(offset)) 62 | 63 | // Get the length of image data to send. 64 | let maxDataLength: UInt = UInt(mtu) - UInt(packetOverhead) 65 | let dataLength: UInt = min(maxDataLength, remainingBytes) 66 | 67 | // Build the request payload. 68 | var payload: [String:CBOR] = ["data": CBOR.byteString([UInt8](data[offset..<(offset+dataLength)])), 69 | "off": CBOR.unsignedInt(UInt64(offset))] 70 | 71 | // 0 is Default behaviour, so we can ignore adding it and 72 | // the firmware will do the right thing. 73 | if image > 0 { 74 | payload.updateValue(CBOR.unsignedInt(UInt64(image)), forKey: "image") 75 | } 76 | 77 | // If this is the initial packet, send the image data length and 78 | // SHA 256 in the payload. 79 | if offset == 0 { 80 | payload.updateValue(CBOR.unsignedInt(UInt64(data.count)), forKey: "len") 81 | payload.updateValue(CBOR.byteString([UInt8](data.sha256()[0..) { 95 | let payload: [String:CBOR] = ["hash": CBOR.byteString(hash), 96 | "confirm": CBOR.boolean(false)] 97 | send(op: .write, commandId: ID_STATE, payload: payload, callback: callback) 98 | } 99 | 100 | /// Confirm the image with the provided hash. 101 | /// 102 | /// A successful confirm will make the image permenant (i.e. the image will 103 | /// be booted upon reset). 104 | /// 105 | /// - parameter hash: The hash of the image to confirm. If not provided, the 106 | /// current image running on the device will be made permenant. 107 | /// - parameter callback: The response callback. 108 | public func confirm(hash: [UInt8]? = nil, callback: @escaping McuMgrCallback) { 109 | var payload: [String:CBOR] = ["confirm": CBOR.boolean(true)] 110 | if let hash = hash { 111 | payload.updateValue(CBOR.byteString(hash), forKey: "hash") 112 | } 113 | send(op: .write, commandId: ID_STATE, payload: payload, callback: callback) 114 | } 115 | 116 | /// Begins the image upload to a peripheral. 117 | /// 118 | /// An instance of ImageManager can only have one upload in progress at a 119 | /// time. Therefore, if this method is called multiple times on the same 120 | /// ImageManager instance, all calls after the first will return false. 121 | /// Upload progress is reported asynchronously to the delegate provided in 122 | /// this method. 123 | /// 124 | /// - parameter data: The entire image data to be uploaded to the peripheral. 125 | /// - parameter image: (Optional) Allows selection of image number for DFU. 126 | /// - parameter delegate: The delegate to recieve progress callbacks. 127 | /// 128 | /// - returns: True if the upload has started successfully, false otherwise. 129 | public func upload(data: Data, image: Int = 0, delegate: ImageUploadDelegate?) -> Bool { 130 | // Make sure two uploads cant start at once. 131 | objc_sync_enter(self) 132 | // If upload is already in progress or paused, do not continue. 133 | if uploadState == .none { 134 | // Set upload flag to true. 135 | uploadState = .uploading 136 | } else { 137 | log(msg: "An image upload is already in progress", atLevel: .warning) 138 | objc_sync_exit(self) 139 | return false 140 | } 141 | objc_sync_exit(self) 142 | 143 | // Set upload delegate. 144 | uploadDelegate = delegate 145 | 146 | // Set image data. 147 | imageData = data 148 | 149 | // Set the slot we're uploading the image to. 150 | imageNumber = image 151 | 152 | // Grab a strong reference to something holding a strong reference to self. 153 | cyclicReferenceHolder = { return self } 154 | 155 | log(msg: "Uploading image (\(data.count) bytes)...", atLevel: .application) 156 | upload(data: imageData!, image: imageNumber, offset: 0, callback: uploadCallback) 157 | return true 158 | } 159 | 160 | /// Erases an unused image from the secondary image slot on the device. 161 | /// 162 | /// The image cannot be erased if the image is a confirmed image, is marked 163 | /// for test on the next reboot, or is an active image for a split image 164 | /// setup. 165 | /// 166 | /// - parameter callback: The response callback. 167 | public func erase(callback: @escaping McuMgrCallback) { 168 | send(op: .write, commandId: ID_ERASE, payload: nil, callback: callback) 169 | } 170 | 171 | /// Erases the state of the secondary image slot on the device. 172 | /// 173 | /// - parameter callback: The response callback. 174 | public func eraseState(callback: @escaping McuMgrCallback) { 175 | send(op: .write, commandId: ID_ERASE_STATE, payload: nil, callback: callback) 176 | } 177 | 178 | /// Requst core dump on the device. The data will be stored in the dump 179 | /// area. 180 | /// 181 | /// - parameter callback: The response callback. 182 | public func coreList(callback: @escaping McuMgrCallback) { 183 | send(op: .read, commandId: ID_CORELIST, payload: nil, callback: callback) 184 | } 185 | 186 | /// Read core dump from the given offset. 187 | /// 188 | /// - parameter offset: The offset to load from, in bytes. 189 | /// - parameter callback: The response callback. 190 | public func coreLoad(offset: UInt, callback: @escaping McuMgrCallback) { 191 | let payload: [String:CBOR] = ["off": CBOR.unsignedInt(UInt64(offset))] 192 | send(op: .read, commandId: ID_CORELOAD, payload: payload, callback: callback) 193 | } 194 | 195 | /// Erase the area if it has a core dump, or the header is empty. 196 | /// 197 | /// - parameter callback: The response callback. 198 | public func coreErase(callback: @escaping McuMgrCallback) { 199 | send(op: .write, commandId: ID_CORELOAD, payload: nil, callback: callback) 200 | } 201 | 202 | //************************************************************************** 203 | // MARK: Image Upload 204 | //************************************************************************** 205 | 206 | /// Image upload states 207 | public enum UploadState: UInt8 { 208 | case none = 0 209 | case uploading = 1 210 | case paused = 2 211 | } 212 | 213 | /// State of the image upload. 214 | private var uploadState: UploadState = .none 215 | /// Image 'slot' or core of the device we're sending data to. 216 | /// Default value, will be secondary slot of core 0. 217 | private var imageNumber: Int = 0 218 | /// Current image byte offset to send from. 219 | private var offset: UInt64 = 0 220 | 221 | /// Contains the image data to send to the device. 222 | private var imageData: Data? 223 | /// Delegate to send image upload updates to. 224 | private weak var uploadDelegate: ImageUploadDelegate? 225 | 226 | /// Cyclic reference is used to prevent from releasing the manager 227 | /// in the middle of an update. The reference cycle will be set 228 | /// when upload was started and released on success, error or cancel. 229 | private var cyclicReferenceHolder: (() -> ImageManager)? 230 | 231 | /// Cancels the current upload. 232 | /// 233 | /// If an error is supplied, the delegate's didFailUpload method will be 234 | /// called with the Upload Error provided. 235 | /// 236 | /// - parameter error: The optional upload error which caused the 237 | /// cancellation. This error (if supplied) is used as the argument for the 238 | /// delegate's didFailUpload method. 239 | public func cancelUpload(error: Error? = nil) { 240 | objc_sync_enter(self) 241 | if uploadState == .none { 242 | log(msg: "Image upload is not in progress", atLevel: .warning) 243 | } else { 244 | if let error = error { 245 | resetUploadVariables() 246 | uploadDelegate?.uploadDidFail(with: error) 247 | uploadDelegate = nil 248 | log(msg: "Upload cancelled due to error: \(error)", atLevel: .error) 249 | // Release cyclic reference. 250 | cyclicReferenceHolder = nil 251 | } else { 252 | if uploadState == .paused { 253 | resetUploadVariables() 254 | uploadDelegate?.uploadDidCancel() 255 | uploadDelegate = nil 256 | log(msg: "Upload cancelled", atLevel: .application) 257 | // Release cyclic reference. 258 | cyclicReferenceHolder = nil 259 | } 260 | // else 261 | // Transfer will be cancelled after the next notification is received. 262 | } 263 | uploadState = .none 264 | } 265 | objc_sync_exit(self) 266 | } 267 | 268 | /// Pauses the current upload. If there is no upload in progress, nothing 269 | /// happens. 270 | public func pauseUpload() { 271 | objc_sync_enter(self) 272 | if uploadState == .none { 273 | log(msg: "Upload is not in progress and therefore cannot be paused", atLevel: .warning) 274 | } else { 275 | uploadState = .paused 276 | log(msg: "Upload paused", atLevel: .application) 277 | } 278 | objc_sync_exit(self) 279 | } 280 | 281 | /// Continues a paused upload. If the upload is not paused or not uploading, 282 | /// nothing happens. 283 | public func continueUpload() { 284 | objc_sync_enter(self) 285 | guard let imageData = imageData else { 286 | objc_sync_exit(self) 287 | if uploadState != .none { 288 | cancelUpload(error: ImageUploadError.invalidData) 289 | } 290 | return 291 | } 292 | if uploadState == .paused { 293 | log(msg: "Continuing upload from \(offset)/\(imageData.count) to image \(imageNumber)...", atLevel: .application) 294 | uploadState = .uploading 295 | upload(data: imageData, image: imageNumber, offset: UInt(offset), callback: uploadCallback) 296 | } else { 297 | log(msg: "Upload has not been previously paused", atLevel: .warning) 298 | } 299 | objc_sync_exit(self) 300 | } 301 | 302 | // MARK: - Image Upload Private Methods 303 | 304 | private lazy var uploadCallback: McuMgrCallback = { 305 | [weak self] (response: McuMgrUploadResponse?, error: Error?) in 306 | // Ensure the manager is not released. 307 | guard let self = self else { 308 | return 309 | } 310 | // Check for an error. 311 | if let error = error { 312 | if case let McuMgrTransportError.insufficientMtu(newMtu) = error { 313 | if !self.setMtu(newMtu) { 314 | self.cancelUpload(error: error) 315 | } else { 316 | self.restartUpload() 317 | } 318 | return 319 | } 320 | self.cancelUpload(error: error) 321 | return 322 | } 323 | // Make sure the image data is set. 324 | guard let imageData = self.imageData else { 325 | self.cancelUpload(error: ImageUploadError.invalidData) 326 | return 327 | } 328 | // Make sure the response is not nil. 329 | guard let response = response else { 330 | self.cancelUpload(error: ImageUploadError.invalidPayload) 331 | return 332 | } 333 | // Check for an error return code. 334 | guard response.isSuccess() else { 335 | self.cancelUpload(error: ImageUploadError.mcuMgrErrorCode(response.returnCode)) 336 | return 337 | } 338 | // Get the offset from the response. 339 | if let offset = response.off { 340 | // Set the image upload offset. 341 | self.offset = offset 342 | self.uploadDelegate?.uploadProgressDidChange(bytesSent: Int(offset), imageSize: imageData.count, timestamp: Date()) 343 | 344 | if self.uploadState == .none { 345 | self.log(msg: "Upload cancelled", atLevel: .application) 346 | self.resetUploadVariables() 347 | self.uploadDelegate?.uploadDidCancel() 348 | self.uploadDelegate = nil 349 | // Release cyclic reference. 350 | self.cyclicReferenceHolder = nil 351 | return 352 | } 353 | 354 | // Check if the upload has completed. 355 | if offset == imageData.count { 356 | self.log(msg: "Upload finished", atLevel: .application) 357 | self.resetUploadVariables() 358 | self.uploadDelegate?.uploadDidFinish() 359 | self.uploadDelegate = nil 360 | // Release cyclic reference. 361 | self.cyclicReferenceHolder = nil 362 | return 363 | } 364 | 365 | // Send the next packet of data. 366 | self.sendNext(from: UInt(offset)) 367 | } else { 368 | self.cancelUpload(error: ImageUploadError.invalidPayload) 369 | } 370 | } 371 | 372 | private func sendNext(from offset: UInt) { 373 | if uploadState != .uploading { 374 | return 375 | } 376 | upload(data: imageData!, image: imageNumber, offset: offset, callback: uploadCallback) 377 | } 378 | 379 | private func resetUploadVariables() { 380 | objc_sync_enter(self) 381 | // Reset upload state. 382 | uploadState = .none 383 | 384 | // Deallocate and nil image data pointers. 385 | imageData = nil 386 | 387 | // Reset upload vars. 388 | imageNumber = 0 389 | offset = 0 390 | objc_sync_exit(self) 391 | } 392 | 393 | private func restartUpload() { 394 | objc_sync_enter(self) 395 | guard let imageData = imageData, let uploadDelegate = uploadDelegate else { 396 | log(msg: "Could not restart upload: image data or callback is null", atLevel: .error) 397 | return 398 | } 399 | let tempData = imageData 400 | let tempDelegate = uploadDelegate 401 | resetUploadVariables() 402 | _ = upload(data: tempData, image: imageNumber, delegate: tempDelegate) 403 | objc_sync_exit(self) 404 | } 405 | 406 | private func calculatePacketOverhead(data: Data, offset: UInt64) -> Int { 407 | // Get the Mcu Manager header. 408 | var payload: [String:CBOR] = ["data": CBOR.byteString([UInt8]([0])), 409 | "off": CBOR.unsignedInt(offset)] 410 | // If this is the initial packet we have to include the length of the 411 | // entire image. 412 | if offset == 0 { 413 | payload.updateValue(CBOR.unsignedInt(UInt64(data.count)), forKey: "len") 414 | payload.updateValue(CBOR.byteString([UInt8](repeating: 0, count: ImageManager.truncatedHashLen)), forKey: "sha") 415 | } 416 | // Build the packet and return the size. 417 | let packet = McuManager.buildPacket(scheme: transporter.getScheme(), op: .write, flags: 0, 418 | group: group.uInt16Value, sequenceNumber: 0, commandId: ID_UPLOAD, payload: payload) 419 | var packetOverhead = packet.count + 5 420 | if transporter.getScheme().isCoap() { 421 | // Add 25 bytes to packet overhead estimate for the CoAP header. 422 | packetOverhead = packetOverhead + 25 423 | } 424 | return packetOverhead 425 | } 426 | } 427 | 428 | public enum ImageUploadError: Error { 429 | /// Response payload values do not exist. 430 | case invalidPayload 431 | /// Image Data is nil. 432 | case invalidData 433 | /// McuMgrResponse contains a error return code. 434 | case mcuMgrErrorCode(McuMgrReturnCode) 435 | } 436 | 437 | extension ImageUploadError: LocalizedError { 438 | 439 | public var errorDescription: String? { 440 | switch self { 441 | case .invalidPayload: 442 | return "Response payload values do not exist." 443 | case .invalidData: 444 | return "Image data is nil." 445 | case .mcuMgrErrorCode(let code): 446 | return "Remote error: \(code)." 447 | } 448 | } 449 | 450 | } 451 | 452 | //****************************************************************************** 453 | // MARK: Image Upload Delegate 454 | //****************************************************************************** 455 | 456 | public protocol ImageUploadDelegate: AnyObject { 457 | 458 | /// Called when a packet of image data has been sent successfully. 459 | /// 460 | /// - parameter bytesSent: The total number of image bytes sent so far. 461 | /// - parameter imageSize: The overall size of the image being uploaded. 462 | /// - parameter timestamp: The time this response packet was received. 463 | func uploadProgressDidChange(bytesSent: Int, imageSize: Int, timestamp: Date) 464 | 465 | /// Called when an image upload has failed. 466 | /// 467 | /// - parameter error: The error that caused the upload to fail. 468 | func uploadDidFail(with error: Error) 469 | 470 | /// Called when the upload has been cancelled. 471 | func uploadDidCancel() 472 | 473 | /// Called when the upload has finished successfully. 474 | func uploadDidFinish() 475 | } 476 | 477 | -------------------------------------------------------------------------------- /Source/Managers/LogManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import SwiftCBOR 9 | 10 | public class LogManager: McuManager { 11 | override class var TAG: McuMgrLogCategory { .log } 12 | 13 | //************************************************************************** 14 | // MARK: Log Constants 15 | //************************************************************************** 16 | 17 | // Mcu Log Manager ids 18 | let ID_READ = UInt8(0) 19 | let ID_CLEAR = UInt8(1) 20 | let ID_APPEND = UInt8(2) 21 | let ID_MODULE_LIST = UInt8(3) 22 | let ID_LEVEL_LIST = UInt8(4) 23 | let ID_LOGS_LIST = UInt8(5) 24 | 25 | //************************************************************************** 26 | // MARK: Initializers 27 | //************************************************************************** 28 | 29 | public init(transporter: McuMgrTransport) { 30 | super.init(group: McuMgrGroup.logs, transporter: transporter) 31 | } 32 | 33 | //************************************************************************** 34 | // MARK: Log Commands 35 | //************************************************************************** 36 | 37 | /// Show logs from a device. 38 | /// 39 | /// Logs will be shown from the log name provided, or all if none. 40 | /// Additionally, logs will only be shown from past the minIndex and 41 | /// minTimestamp if provided. The minimum timestamp will only be accounted 42 | /// for if the minIndex is also provided. 43 | /// 44 | /// This method will only provide a portion of the logs, and return the next 45 | /// index to pull the logs from. Therefore, in order to pull all of the logs 46 | /// from the device, you may have to call this method multiple times. 47 | /// 48 | /// - parameter log: The name of the log to read from. 49 | /// - parameter minIndex: The optional minimum index to pull logs from. If 50 | /// not provided, the device will read the oldest log. 51 | /// - parameter minTimestamp: The minimum timestamp to pull logs from. This 52 | /// parameter is only used if a minIndex is also provided. 53 | /// - parameter callback: The response callback. 54 | public func show(log: String? = nil, minIndex: UInt64? = nil, minTimestamp: Date? = nil, callback: @escaping McuMgrCallback) { 55 | var payload: [String:CBOR] = [:] 56 | if let log = log { 57 | payload.updateValue(CBOR.utf8String(log), forKey: "log_name") 58 | } 59 | if let minIndex = minIndex { 60 | payload.updateValue(CBOR.unsignedInt(minIndex), forKey: "index") 61 | if let minTimestamp = minTimestamp { 62 | payload.updateValue(CBOR.utf8String(McuManager.dateToString(date: minTimestamp)), forKey: "ts") 63 | } 64 | } 65 | send(op: .read, commandId: ID_READ, payload: payload, callback: callback) 66 | } 67 | 68 | /// Clear the logs on a device. 69 | /// 70 | /// - parameter callback: The response callback. 71 | public func clear(callback: @escaping McuMgrCallback) { 72 | send(op: .write, commandId: ID_CLEAR, payload: nil, callback: callback) 73 | } 74 | 75 | /// List the log modules on a device. 76 | /// 77 | /// - parameter callback: The response callback. 78 | public func moduleList(callback: @escaping McuMgrCallback) { 79 | send(op: .read, commandId: ID_MODULE_LIST, payload: nil, callback: callback) 80 | } 81 | 82 | /// List the log levels on a device. 83 | /// 84 | /// - parameter callback: The response callback. 85 | public func levelList(callback: @escaping McuMgrCallback) { 86 | send(op: .read, commandId: ID_LEVEL_LIST, payload: nil, callback: callback) 87 | } 88 | 89 | /// List the logs on a device. 90 | /// 91 | /// - parameter callback: The response callback. 92 | public func logsList(callback: @escaping McuMgrCallback) { 93 | send(op: .read, commandId: ID_LOGS_LIST, payload: nil, callback: callback) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Source/Managers/RunTestManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import SwiftCBOR 9 | 10 | public class RunTestManager: McuManager { 11 | override class var TAG: McuMgrLogCategory { .runTest } 12 | 13 | //************************************************************************** 14 | // MARK: Run Constants 15 | //************************************************************************** 16 | 17 | // Mcu Run Test Manager ids. 18 | let ID_TEST = UInt8(0) 19 | let ID_LIST = UInt8(1) 20 | 21 | //************************************************************************** 22 | // MARK: Initializers 23 | //************************************************************************** 24 | 25 | public init(transporter: McuMgrTransport) { 26 | super.init(group: McuMgrGroup.run, transporter: transporter) 27 | } 28 | 29 | //************************************************************************** 30 | // MARK: Run Commands 31 | //************************************************************************** 32 | 33 | /// Run tests on a device. 34 | /// 35 | /// The device will run the test specified in the 'name' or all tests if not 36 | /// specified. 37 | /// 38 | /// - parameter name: The name of the test to run. If left out, all tests 39 | /// will be run. 40 | /// - parameter token: The optional token to returned in the response. 41 | /// - parameter callback: The response callback. 42 | public func test(name: String? = nil, token: String? = nil, callback: @escaping McuMgrCallback) { 43 | var payload: [String:CBOR] = [:] 44 | if let name = name { 45 | payload.updateValue(CBOR.utf8String(name), forKey: "testname") 46 | } 47 | if let token = token { 48 | payload.updateValue(CBOR.utf8String(token), forKey: "token") 49 | } 50 | send(op: .write, commandId: ID_TEST, payload: payload, callback: callback) 51 | } 52 | 53 | /// List the tests on a device. 54 | /// 55 | /// - parameter callback: The response callback. 56 | public func list(callback: @escaping McuMgrCallback) { 57 | send(op: .read, commandId: ID_LIST, payload: nil, callback: callback) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Source/Managers/StatsManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import SwiftCBOR 9 | 10 | /// Displays statistics from a device. 11 | /// 12 | /// Stats manager can read the list of stats modules from a device and read the 13 | /// statistics from a specific module. 14 | public class StatsManager: McuManager { 15 | override class var TAG: McuMgrLogCategory { .stats } 16 | 17 | //************************************************************************** 18 | // MARK: Stats Constants 19 | //************************************************************************** 20 | 21 | // Mcu Stats Manager ids. 22 | let ID_READ = UInt8(0) 23 | let ID_LIST = UInt8(1) 24 | 25 | //************************************************************************** 26 | // MARK: Initializers 27 | //************************************************************************** 28 | 29 | public init(transporter: McuMgrTransport) { 30 | super.init(group: McuMgrGroup.stats, transporter: transporter) 31 | } 32 | 33 | //************************************************************************** 34 | // MARK: Stats Commands 35 | //************************************************************************** 36 | 37 | /// Read statistics from a particular stats module. 38 | /// 39 | /// - parameter module: The statistics module to. 40 | /// - parameter callback: The response callback. 41 | public func read(module: String, callback: @escaping McuMgrCallback) { 42 | let payload: [String:CBOR] = ["name": CBOR.utf8String(module)] 43 | send(op: .read, commandId: ID_READ, payload: payload, callback: callback) 44 | } 45 | 46 | /// List the statistic modules from a device. 47 | /// 48 | /// - parameter callback: The response callback. 49 | public func list(callback: @escaping McuMgrCallback) { 50 | send(op: .read, commandId: ID_LIST, payload: nil, callback: callback) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Source/McuManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | import CoreBluetooth 9 | import SwiftCBOR 10 | 11 | open class McuManager { 12 | class var TAG: McuMgrLogCategory { .default } 13 | 14 | //************************************************************************** 15 | // MARK: Mcu Manager Constants 16 | //************************************************************************** 17 | 18 | /// Mcu Manager CoAP Resource URI. 19 | public static let COAP_PATH = "/omgr" 20 | 21 | /// Header Key for CoAP Payloads. 22 | public static let HEADER_KEY = "_h" 23 | 24 | //************************************************************************** 25 | // MARK: Properties 26 | //************************************************************************** 27 | 28 | /// Handles transporting Mcu Manager commands. 29 | public let transporter: McuMgrTransport 30 | 31 | /// The command group used for in the header of commands sent using this Mcu 32 | /// Manager. 33 | public let group: McuMgrGroup 34 | 35 | /// The MTU used by this manager. This value must be between 23 and 1024. 36 | /// The MTU is usually only a factor when uploading files or images to the 37 | /// device, where each request should attempt to maximize the amount of 38 | /// data being sent to the device. 39 | public var mtu: Int 40 | 41 | /// Logger delegate will receive logs. 42 | public weak var logDelegate: McuMgrLogDelegate? 43 | 44 | //************************************************************************** 45 | // MARK: Initializers 46 | //************************************************************************** 47 | 48 | public init(group: McuMgrGroup, transporter: McuMgrTransport) { 49 | self.group = group 50 | self.transporter = transporter 51 | self.mtu = McuManager.getDefaultMtu(scheme: transporter.getScheme()) 52 | } 53 | 54 | //************************************************************************** 55 | // MARK: Send Commands 56 | //************************************************************************** 57 | 58 | public func send(op: McuMgrOperation, 59 | commandId: UInt8, 60 | payload: [String:CBOR]?, 61 | callback: @escaping McuMgrCallback) { 62 | send(op: op, flags: 0, sequenceNumber: 0, commandId: commandId, 63 | payload: payload, callback: callback) 64 | } 65 | 66 | public func send(op: McuMgrOperation, flags: UInt8, 67 | sequenceNumber: UInt8, 68 | commandId: UInt8, 69 | payload: [String:CBOR]?, 70 | callback: @escaping McuMgrCallback) { 71 | log(msg: "Sending \(op) command (Group: \(group), ID: \(commandId)): \(payload?.debugDescription ?? "nil")", 72 | atLevel: .verbose) 73 | let data = McuManager.buildPacket(scheme: transporter.getScheme(), op: op, 74 | flags: flags, group: group.uInt16Value, 75 | sequenceNumber: sequenceNumber, 76 | commandId: commandId, payload: payload) 77 | let _callback: McuMgrCallback = logDelegate == nil ? callback : { [weak self] (response, error) in 78 | if let self = self { 79 | if let response = response { 80 | self.log(msg: "Response (Group: \(self.group), ID: \(response.header!.commandId!)): \(response)", 81 | atLevel: .verbose) 82 | } else if let error = error { 83 | self.log(msg: "Request failed: \(error.localizedDescription)", 84 | atLevel: .error) 85 | } 86 | } 87 | callback(response, error) 88 | } 89 | send(data: data, callback: _callback) 90 | } 91 | 92 | public func send(data: Data, callback: @escaping McuMgrCallback) { 93 | transporter.send(data: data, callback: callback) 94 | } 95 | 96 | //************************************************************************** 97 | // MARK: Build Request Packet 98 | //************************************************************************** 99 | 100 | /// Build a McuManager request packet based on the transporter scheme. 101 | /// 102 | /// - parameter scheme: The transport scheme. 103 | /// - parameter op: The McuManagerOperation code. 104 | /// - parameter flags: The optional flags. 105 | /// - parameter group: The command group. 106 | /// - parameter sequenceNumber: The optional sequence number. 107 | /// - parameter commandId: The command id. 108 | /// - parameter payload: The request payload. 109 | /// 110 | /// - returns: The raw packet data to send to the transporter. 111 | public static func buildPacket(scheme: McuMgrScheme, op: McuMgrOperation, flags: UInt8, 112 | group: UInt16, sequenceNumber: UInt8, 113 | commandId: UInt8, payload: [String:CBOR]?) -> Data { 114 | // If the payload map is nil, initialize an empty map. 115 | var payload = (payload == nil ? [:] : payload)! 116 | 117 | // Copy the payload map to remove the header key. 118 | var payloadCopy = payload 119 | // Remove the header if present (for CoAP schemes). 120 | payloadCopy.removeValue(forKey: McuManager.HEADER_KEY) 121 | 122 | // Get the length. 123 | let len: UInt16 = UInt16(CBOR.encode(payloadCopy).count) 124 | 125 | // Build header. 126 | let header = McuMgrHeader.build(op: op.rawValue, flags: flags, len: len, 127 | group: group, seq: sequenceNumber, 128 | id: commandId) 129 | 130 | // Build the packet based on scheme. 131 | if scheme.isCoap() { 132 | // CoAP transport schemes puts the header as a key-value pair in the 133 | // payload. 134 | if payload[McuManager.HEADER_KEY] == nil { 135 | payload.updateValue(CBOR.byteString(header), forKey: McuManager.HEADER_KEY) 136 | } 137 | return Data(CBOR.encode(payload)) 138 | } else { 139 | // Standard scheme appends the CBOR payload to the header. 140 | let cborPayload = CBOR.encode(payload) 141 | var packet = Data(header) 142 | packet.append(contentsOf: cborPayload) 143 | return packet 144 | } 145 | } 146 | 147 | //************************************************************************** 148 | // MARK: Utilities 149 | //************************************************************************** 150 | 151 | /// Converts a date and optional timezone to a string which Mcu Manager on 152 | /// the device can use. 153 | /// 154 | /// The date format used is: "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 155 | /// 156 | /// - parameter date: The date. 157 | /// - parameter timeZone: Optional timezone for the given date. If left out 158 | /// or nil, the timzone will be set to the system time zone. 159 | /// 160 | /// - returns: The datetime string. 161 | public static func dateToString(date: Date, timeZone: TimeZone? = nil) -> String { 162 | let RFC3339DateFormatter = DateFormatter() 163 | RFC3339DateFormatter.locale = Locale(identifier: "en_US_POSIX") 164 | RFC3339DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 165 | RFC3339DateFormatter.timeZone = (timeZone != nil ? timeZone : TimeZone.current) 166 | return RFC3339DateFormatter.string(from: date) 167 | } 168 | 169 | /// Set the MTU used by this McuManager. The McuManager MTU must be between 170 | /// 23 and 1024 (inclusive). The MTU generally only matters when uploading 171 | /// to the device, where the upload data in each request should be 172 | /// maximized. 173 | /// 174 | /// - parameter mtu: The mtu to set. 175 | /// 176 | /// - returns: true if the value is between 23 and 1024 (inclusive), false 177 | /// otherwise 178 | public func setMtu(_ mtu: Int) -> Bool { 179 | if mtu >= 23 && mtu <= 1024 { 180 | self.mtu = mtu 181 | log(msg: "MTU set to \(mtu)", atLevel: .debug) 182 | return true 183 | } else { 184 | log(msg: "Invalid MTU (\(mtu)): Value must be between 23 and 1024", atLevel: .warning) 185 | return false 186 | } 187 | } 188 | 189 | /// Get the default MTU which should be used for a transport scheme. If the 190 | /// scheme is BLE, the iOS version is used to determine the MTU. If the 191 | /// scheme is UDP, the MTU returned is always 1024. 192 | /// 193 | /// - parameter scheme: the transporter 194 | public static func getDefaultMtu(scheme: McuMgrScheme) -> Int { 195 | // BLE MTU is determined by the version of iOS running on the device 196 | if scheme.isBle() { 197 | /// Return the maximum BLE ATT MTU for this iOS device. 198 | if #available(iOS 11.0, *) { 199 | // For iOS 11.0+ (527 - 3) 200 | return 524 201 | } else if #available(iOS 10.0, *) { 202 | // For iOS 10.0 (185 - 3) 203 | return 182 204 | } else { 205 | // For iOS 9.0 (158 - 3) 206 | return 155 207 | } 208 | } else { 209 | return 1024 210 | } 211 | } 212 | } 213 | 214 | extension McuManager { 215 | 216 | func log(msg: String, atLevel level: McuMgrLogLevel) { 217 | logDelegate?.log(msg, ofCategory: Self.TAG, atLevel: level) 218 | } 219 | 220 | } 221 | 222 | /// McuManager callback 223 | public typealias McuMgrCallback = (T?, Error?) -> Void 224 | 225 | /// The defined groups for Mcu Manager commands. 226 | /// 227 | /// Each group has its own manager class which contains the specific subcommands 228 | /// and functions. The default are contained within the McuManager class. 229 | public enum McuMgrGroup { 230 | /// Default command group (DefaultManager). 231 | case `default` 232 | /// Image command group (ImageManager). 233 | case image 234 | /// Statistics command group (StatsManager). 235 | case stats 236 | /// System configuration command group (ConfigManager). 237 | case config 238 | /// Log command group (LogManager). 239 | case logs 240 | /// Crash command group (CrashManager). 241 | case crash 242 | /// Split image command group (Not implemented). 243 | case split 244 | /// Run test command group (RunManager). 245 | case run 246 | /// File System command group (FileSystemManager). 247 | case fs 248 | /// Per user command group, value must be >= 64. 249 | case peruser(value: UInt16) 250 | 251 | var uInt16Value: UInt16 { 252 | switch self { 253 | case .default: return 0 254 | case .image: return 1 255 | case .stats: return 2 256 | case .config: return 3 257 | case .logs: return 4 258 | case .crash: return 5 259 | case .split: return 6 260 | case .run: return 7 261 | case .fs: return 8 262 | case .peruser(let value): return value 263 | } 264 | } 265 | } 266 | 267 | /// The mcu manager operation defines whether the packet sent is a read/write 268 | /// and request/response. 269 | public enum McuMgrOperation: UInt8 { 270 | case read = 0 271 | case readResponse = 1 272 | case write = 2 273 | case writeResponse = 3 274 | } 275 | 276 | /// Return codes for Mcu Manager responses. 277 | /// 278 | /// Each Mcu Manager response will contain a "rc" key with one of these return 279 | /// codes. 280 | public enum McuMgrReturnCode: UInt64, Error { 281 | case ok = 0 282 | case unknown = 1 283 | case noMemory = 2 284 | case inValue = 3 285 | case timeout = 4 286 | case noEntry = 5 287 | case badState = 6 288 | case unrecognized 289 | 290 | public func isSuccess() -> Bool { 291 | return self == .ok 292 | } 293 | 294 | public func isError() -> Bool { 295 | return self != .ok 296 | } 297 | } 298 | 299 | extension McuMgrReturnCode: CustomStringConvertible { 300 | 301 | public var description: String { 302 | switch self { 303 | case .ok: 304 | return "OK (0)" 305 | case .unknown: 306 | return "Unknown (1)" 307 | case .noMemory: 308 | return "No Memory (2)" 309 | case .inValue: 310 | return "In Value (3)" 311 | case .timeout: 312 | return "Timeout (4)" 313 | case .noEntry: 314 | return "No Entry (5)" 315 | case .badState: 316 | return "Bad State (1)" 317 | default: 318 | return "Unrecognized (\(rawValue))" 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /Source/McuMgrHeader.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Represents an 8-byte McuManager header. 10 | public class McuMgrHeader { 11 | 12 | /// Header length. 13 | public static let HEADER_LENGTH = 8 14 | 15 | public let op: UInt8! 16 | public let flags: UInt8! 17 | public let length: UInt16! 18 | public let groupId: UInt16! 19 | public let sequenceNumber: UInt8! 20 | public let commandId: UInt8! 21 | 22 | /// Initialize the header with raw data. Because this method only parses the 23 | /// first eight bytes of the input data, the data's count must be greater or 24 | /// equal than eight. 25 | /// 26 | /// - parameter data: The data to parse. Data count must be greater than or 27 | /// equal to eight. 28 | /// - throws: McuMgrHeaderParseException.invalidSize(Int) if the data count 29 | /// is too small. 30 | public init(data: Data) throws { 31 | if (data.count < McuMgrHeader.HEADER_LENGTH) { 32 | throw McuMgrHeaderParseError.invalidSize(data.count) 33 | } 34 | op = data[0] 35 | flags = data[1] 36 | length = data.readBigEndian(offset: 2) 37 | groupId = data.readBigEndian(offset: 4) 38 | sequenceNumber = data[6] 39 | commandId = data[7] 40 | } 41 | 42 | public init(op: UInt8, flags: UInt8, length: UInt16, groupId: UInt16, sequenceNumber: UInt8, commandId: UInt8) { 43 | self.op = op 44 | self.flags = flags 45 | self.length = length 46 | self.groupId = groupId 47 | self.sequenceNumber = sequenceNumber 48 | self.commandId = commandId 49 | } 50 | 51 | public func toData() -> Data { 52 | var data = Data(count: McuMgrHeader.HEADER_LENGTH) 53 | data.append(op) 54 | data.append(flags) 55 | data.append(Data(from: length)) 56 | data.append(Data(from: groupId)) 57 | data.append(sequenceNumber) 58 | data.append(commandId) 59 | return data 60 | } 61 | 62 | /// Helper function to build a raw mcu manager header. 63 | /// 64 | /// - parameter op: The Mcu Manager operation. 65 | /// - parameter flags: Optional flags. 66 | /// - parameter len: Optional length. 67 | /// - parameter group: The group id for this command. 68 | /// - parameter seq: Optional sequence number. 69 | /// - parameter id: The subcommand id for the given group. 70 | public static func build(op: UInt8, flags: UInt8, len: UInt16, group: UInt16, seq: UInt8, id: UInt8) -> [UInt8] { 71 | return [op, flags, UInt8(len >> 8), UInt8(len & 0xFF), UInt8(group >> 8), UInt8(group & 0xFF), seq, id] 72 | } 73 | } 74 | 75 | extension McuMgrHeader: CustomDebugStringConvertible { 76 | 77 | public var debugDescription: String { 78 | return "{\"op\": \"\(op!)\", \"flags\": \(flags!), \"length\": \(length!), \"group\": \(groupId!), \"seqNum\": \(sequenceNumber!), \"commandId\": \(commandId!)}" 79 | } 80 | 81 | } 82 | 83 | public enum McuMgrHeaderParseError: Error { 84 | case invalidSize(Int) 85 | } 86 | 87 | extension McuMgrHeaderParseError: LocalizedError { 88 | 89 | public var errorDescription: String? { 90 | switch self { 91 | case .invalidSize(let size): 92 | return "Invalid header size: \(size) (expected: \(McuMgrHeader.HEADER_LENGTH))." 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /Source/McuMgrImage.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | 9 | public class McuMgrImage { 10 | 11 | public static let IMG_HASH_LEN = 32 12 | 13 | public let header: McuMgrImageHeader 14 | public let tlv: McuMgrImageTlv 15 | public let data: Data 16 | public let hash: Data 17 | 18 | public init(data: Data) throws { 19 | self.data = data 20 | self.header = try McuMgrImageHeader(data: data) 21 | self.tlv = try McuMgrImageTlv(data: data, imageHeader: header) 22 | self.hash = tlv.hash 23 | } 24 | } 25 | 26 | public class McuMgrImageHeader { 27 | 28 | public static let IMG_HEADER_LEN = 24 29 | 30 | public static let IMG_HEADER_MAGIC: UInt32 = 0x96f3b83d 31 | public static let IMG_HEADER_MAGIC_V1: UInt32 = 0x96f3b83c 32 | 33 | public static let MAGIC_OFFSET = 0 34 | public static let LOAD_ADDR_OFFSET = 4 35 | public static let HEADER_SIZE_OFFSET = 8 36 | public static let IMAGE_SIZE_OFFSET = 12 37 | public static let FLAGS_OFFSET = 16 38 | 39 | public let magic: UInt32 40 | public let loadAddr: UInt32 41 | public let headerSize: UInt16 42 | // __pad1: UInt16 43 | public let imageSize: UInt32 44 | public let flags: UInt32 45 | public let version: McuMgrImageVersion 46 | // __pad2 UInt16 47 | 48 | public init(data: Data) throws { 49 | magic = data.read(offset: McuMgrImageHeader.MAGIC_OFFSET) 50 | loadAddr = data.read(offset: McuMgrImageHeader.LOAD_ADDR_OFFSET) 51 | headerSize = data.read(offset: McuMgrImageHeader.HEADER_SIZE_OFFSET) 52 | imageSize = data.read(offset: McuMgrImageHeader.IMAGE_SIZE_OFFSET) 53 | flags = data.read(offset: McuMgrImageHeader.FLAGS_OFFSET) 54 | version = McuMgrImageVersion(data: data) 55 | if magic != McuMgrImageHeader.IMG_HEADER_MAGIC && magic != McuMgrImageHeader.IMG_HEADER_MAGIC_V1 { 56 | throw McuMgrImageParseError.invalidHeaderMagic 57 | } 58 | } 59 | 60 | public func isLegacy() -> Bool { 61 | return magic == McuMgrImageHeader.IMG_HEADER_MAGIC_V1 62 | } 63 | } 64 | 65 | public class McuMgrImageVersion { 66 | 67 | public static let VERSION_OFFSET = 20 68 | 69 | public let major: UInt8 70 | public let minor: UInt8 71 | public let revision: UInt16 72 | public let build: UInt32 73 | 74 | public init(data: Data, offset: Int = VERSION_OFFSET) { 75 | major = data[offset] 76 | minor = data[offset + 1] 77 | revision = data.read(offset: offset + 2) 78 | build = data.read(offset: offset + 4) 79 | } 80 | } 81 | 82 | public class McuMgrImageTlv { 83 | 84 | public static let IMG_TLV_SHA256: UInt8 = 0x10 85 | public static let IMG_TLV_SHA256_V1: UInt8 = 0x01 86 | public static let IMG_TLV_INFO_MAGIC: UInt16 = 0x6907 87 | 88 | public var tlvInfo: McuMgrImageTlvInfo? 89 | public var trailerTlvEntries: [McuMgrImageTlvTrailerEntry] 90 | 91 | public let hash: Data 92 | 93 | public init(data: Data, imageHeader: McuMgrImageHeader) throws { 94 | var offset = Int(imageHeader.headerSize) + Int(imageHeader.imageSize) 95 | let end = data.count 96 | 97 | // Parse the tlv info header (Not included in legacy version). 98 | if !imageHeader.isLegacy() { 99 | try tlvInfo = McuMgrImageTlvInfo(data: data, offset: offset) 100 | offset += McuMgrImageTlvInfo.SIZE 101 | } 102 | 103 | // Parse each tlv entry. 104 | trailerTlvEntries = [McuMgrImageTlvTrailerEntry]() 105 | var hashEntry: McuMgrImageTlvTrailerEntry? 106 | while offset + McuMgrImageTlvTrailerEntry.MIN_SIZE < end { 107 | let tlvEntry = try McuMgrImageTlvTrailerEntry(data: data, offset: offset) 108 | trailerTlvEntries.append(tlvEntry) 109 | // Set the hash if this entry's type matches the hash's type 110 | if imageHeader.isLegacy() && tlvEntry.type == McuMgrImageTlv.IMG_TLV_SHA256_V1 || 111 | !imageHeader.isLegacy() && tlvEntry.type == McuMgrImageTlv.IMG_TLV_SHA256 { 112 | hashEntry = tlvEntry 113 | } 114 | 115 | // Increment offset. 116 | offset += tlvEntry.size 117 | } 118 | 119 | // Set the hash. If not found, throw an error. 120 | if let hashEntry = hashEntry { 121 | hash = hashEntry.value 122 | } else { 123 | throw McuMgrImageParseError.hashNotFound 124 | } 125 | } 126 | } 127 | 128 | /// Represents the header which starts immediately after the image data and 129 | /// precedes the image trailer TLV. 130 | public class McuMgrImageTlvInfo { 131 | 132 | public static let SIZE = 4 133 | 134 | public let magic: UInt16 135 | public let total: UInt16 136 | 137 | public init(data: Data, offset: Int) throws { 138 | magic = data.read(offset: offset) 139 | total = data.read(offset: offset + 2) 140 | if magic != McuMgrImageTlv.IMG_TLV_INFO_MAGIC { 141 | throw McuMgrImageParseError.invalidTlvInfoMagic 142 | } 143 | } 144 | } 145 | 146 | /// Represents an entry in the image TLV trailer. 147 | public class McuMgrImageTlvTrailerEntry { 148 | 149 | /// The minimum size of the TLV entry (length = 0). 150 | public static let MIN_SIZE = 4 151 | 152 | public let type: UInt8 153 | // __pad: UInt8 154 | public let length: UInt16 155 | public let value: Data 156 | 157 | /// Size of the entire TLV entry in bytes. 158 | public let size: Int 159 | 160 | public init(data: Data, offset: Int) throws { 161 | guard offset + McuMgrImageTlvTrailerEntry.MIN_SIZE < data.count else { 162 | throw McuMgrImageParseError.insufficientData 163 | } 164 | 165 | var offset = offset 166 | type = data[offset] 167 | offset += 2 // Increment offset and account for extra byte of padding. 168 | length = data.read(offset: offset) 169 | offset += 2 // Move offset past length. 170 | value = data[Int(offset).. Bool { 15 | return self != .ble 16 | } 17 | 18 | func isBle() -> Bool { 19 | return self != .coapUdp 20 | } 21 | } 22 | 23 | public enum McuMgrTransportState { 24 | case connected, disconnected 25 | } 26 | 27 | /// The connection state observer protocol. 28 | public protocol ConnectionObserver: AnyObject { 29 | /// Called whenever the peripheral state changes. 30 | /// 31 | /// - parameter transport: the Mcu Mgr transport object. 32 | /// - parameter state: The new state of the peripheral. 33 | func transport(_ transport: McuMgrTransport, didChangeStateTo state: McuMgrTransportState) 34 | } 35 | 36 | public enum ConnectionResult { 37 | case connected 38 | case deferred 39 | case failed(Error) 40 | } 41 | 42 | public typealias ConnectionCallback = (ConnectionResult) -> Void 43 | 44 | public enum McuMgrTransportError: Error { 45 | /// Connection to the remote device has timed out. 46 | case connectionTimeout 47 | /// Connection to the remote device has failed. 48 | case connectionFailed 49 | /// Device has disconnected. 50 | case disconnected 51 | /// Sending the request to the device has timed out. 52 | case sendTimeout 53 | /// Sending the request to the device has failed. 54 | case sendFailed 55 | /// The transport MTU is insufficient to send the request. The transport's 56 | /// MTU must be sent back as this case's argument. 57 | case insufficientMtu(mtu: Int) 58 | /// The response received was bad. 59 | case badResponse 60 | } 61 | 62 | extension McuMgrTransportError: LocalizedError { 63 | 64 | public var errorDescription: String? { 65 | switch self { 66 | case .connectionTimeout: 67 | return "Connection timed out." 68 | case .connectionFailed: 69 | return "Connection failed." 70 | case .disconnected: 71 | return "Device disconnected unexpectedly." 72 | case .sendTimeout: 73 | return "Sending the request timed out." 74 | case .sendFailed: 75 | return "Sending the request failed." 76 | case .insufficientMtu(mtu: let mtu): 77 | return "Insufficient MTU: \(mtu)." 78 | case .badResponse: 79 | return "Bad response received." 80 | } 81 | } 82 | } 83 | 84 | /// Mcu Mgr transport object. The transport object 85 | /// should automatically handle connection on first request. 86 | public protocol McuMgrTransport: AnyObject { 87 | /// Returns the transport scheme. 88 | /// 89 | /// - returns: The transport scheme. 90 | func getScheme() -> McuMgrScheme 91 | 92 | /// Sends given data using the transport object. 93 | /// 94 | /// - parameter data: The data to be sent. 95 | /// - parameter callback: The request callback. 96 | func send(data: Data, callback: @escaping McuMgrCallback) 97 | 98 | /// Set up a connection to the remote device. 99 | func connect(_ callback: @escaping ConnectionCallback) 100 | 101 | /// Releases the transport object. This should disconnect the peripheral. 102 | func close() 103 | 104 | /// Adds the connection state observer. 105 | /// 106 | /// - parameter observer: The observer to be added. 107 | func addObserver(_ observer: ConnectionObserver); 108 | 109 | /// Removes the connection state observer. 110 | /// 111 | /// - parameter observer: The observer to be removed. 112 | func removeObserver(_ observer: ConnectionObserver); 113 | } 114 | -------------------------------------------------------------------------------- /Source/Utils/CBOR+String.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import SwiftCBOR 8 | #if canImport(Foundation) 9 | import Foundation 10 | #endif 11 | 12 | extension CBOR: CustomDebugStringConvertible { 13 | 14 | public var debugDescription: String { 15 | switch self { 16 | case .unsignedInt(let value): return "\(value)" 17 | case .negativeInt(let value): return "\(value)" 18 | case .byteString(let bytes): 19 | if bytes.isEmpty { 20 | return "0 bytes" 21 | } 22 | return "0x\(bytes.map { String(format: "%02X", $0) }.joined())" 23 | case .utf8String(let value): return "\"\(value)\"" 24 | case .array(let array): 25 | return "[" + array 26 | .map { $0.debugDescription } 27 | .joined(separator: ", ") + "]" 28 | case .map(let map): 29 | return "{" + map 30 | .map { key, value in 31 | // This will print the "rc" in human readable format. 32 | if case .utf8String(let k) = key, k == "rc", 33 | case .unsignedInt(let v) = value, 34 | let status = McuMgrReturnCode(rawValue: v) { 35 | return "\(key) : \(status)" 36 | } 37 | return "\(key.debugDescription) : \(value.debugDescription)" 38 | } 39 | .joined(separator: ", ") + "}" 40 | case .tagged(let tag, let cbor): 41 | return "\(tag): \(cbor)" 42 | case .simple(let value): return "\(value)" 43 | case .boolean(let value): return "\(value)" 44 | case .null: return "null" 45 | case .undefined: return "undefined" 46 | case .half(let value): return "\(value)" 47 | case .float(let value): return "\(value)" 48 | case .double(let value): return "\(value)" 49 | case .`break`: return "break" 50 | #if canImport(Foundation) 51 | case .date(let value): return "\(value)" 52 | #endif 53 | } 54 | } 55 | 56 | } 57 | 58 | extension Dictionary where Key == CBOR, Value == CBOR { 59 | 60 | // This overridden description takes care of printing the "rc" (Return Code) 61 | // in human readable format. All other values are printed as normal. 62 | public var description: String { 63 | return "{" + 64 | map { key, value in 65 | if case .utf8String(let k) = key, k == "rc", 66 | case .unsignedInt(let v) = value, 67 | let status = McuMgrReturnCode(rawValue: v) { 68 | return "\(key) : \(status)" 69 | } 70 | return "\(key.description) : \(value.description)" 71 | } 72 | .joined(separator: ", ") 73 | + "}" 74 | } 75 | 76 | } 77 | 78 | extension CBOR.Tag: CustomDebugStringConvertible { 79 | 80 | public var debugDescription: String { 81 | switch rawValue { 82 | case CBOR.Tag.standardDateTimeString.rawValue: 83 | return "Standard Date Time String" 84 | case CBOR.Tag.epochBasedDateTime.rawValue: 85 | return "Epoch Based Date Time" 86 | case CBOR.Tag.positiveBignum.rawValue: 87 | return "Positive Big Number" 88 | case CBOR.Tag.negativeBignum.rawValue: 89 | return "Negative Big Number" 90 | case CBOR.Tag.decimalFraction.rawValue: 91 | return "Decimal Fraction" 92 | case CBOR.Tag.bigfloat.rawValue: 93 | return "Big Float" 94 | case CBOR.Tag.expectedConversionToBase64URLEncoding.rawValue: 95 | return "Expected Conversion oo Base64 URL Encoding" 96 | case CBOR.Tag.expectedConversionToBase64Encoding.rawValue: 97 | return "Expected Conversion to Base64 Encoding" 98 | case CBOR.Tag.expectedConversionToBase16Encoding.rawValue: 99 | return "Expected Conversion to Base16 Encoding" 100 | case CBOR.Tag.encodedCBORDataItem.rawValue: 101 | return "Encoded CBOR Data Item" 102 | case CBOR.Tag.uri.rawValue: 103 | return "URI" 104 | case CBOR.Tag.base64Url.rawValue: 105 | return "Base64 URL" 106 | case CBOR.Tag.base64.rawValue: 107 | return "Base64" 108 | case CBOR.Tag.regularExpression.rawValue: 109 | return "Regular Expression" 110 | case CBOR.Tag.mimeMessage.rawValue: 111 | return "MIME Message" 112 | case CBOR.Tag.uuid.rawValue: 113 | return "UUID" 114 | case CBOR.Tag.selfDescribeCBOR.rawValue: 115 | return "Self Describe CBOR" 116 | default: 117 | return "tag(\(rawValue))" 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /Source/Utils/ResultLock.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018 Runtime Inc. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Foundation 8 | 9 | public enum LockResult { 10 | case timeout 11 | case success 12 | case error(Error) 13 | } 14 | 15 | public typealias ResultLockKey = String 16 | 17 | public class ResultLock { 18 | 19 | private var semaphore: DispatchSemaphore 20 | 21 | public var isOpen: Bool = false 22 | public var error: Error? 23 | public var key: ResultLockKey? 24 | 25 | public init(isOpen: Bool) { 26 | self.isOpen = isOpen 27 | self.semaphore = DispatchSemaphore(value: 0) 28 | } 29 | 30 | /// Block the current thread until the condition is opened. 31 | /// 32 | /// If the condition is already opened, return immediately. 33 | public func block() -> LockResult { 34 | if !isOpen { 35 | semaphore.wait() 36 | } 37 | if error != nil { 38 | return .error(error!) 39 | } else { 40 | return .success 41 | } 42 | } 43 | 44 | /// Block the current thread until the condition is opened or until timeout. 45 | /// 46 | /// If the condition is opened, return immediately. 47 | public func block(timeout: DispatchTime) -> LockResult { 48 | let dispatchTimeoutResult: DispatchTimeoutResult 49 | if !isOpen { 50 | dispatchTimeoutResult = semaphore.wait(timeout: timeout) 51 | } else { 52 | dispatchTimeoutResult = .success 53 | } 54 | 55 | if dispatchTimeoutResult == .timedOut { 56 | return .timeout 57 | } else if error != nil { 58 | return .error(error!) 59 | } else { 60 | return .success 61 | } 62 | } 63 | 64 | /// Open the condition, and release all threads that are blocked 65 | /// only if the provided key is the same that closed it, or if no key was used to close it. 66 | /// 67 | /// Any threads that later approach block() will not block unless close() is called. 68 | public func open(key: ResultLockKey) { 69 | let canOpen = (self.key == nil) || (key == self.key) 70 | guard canOpen else { return } 71 | open() 72 | } 73 | 74 | /// Open the condition, and release all threads that are blocked. 75 | /// 76 | /// Any threads that later approach block() will not block unless close() is called. 77 | public func open(_ error: Error? = nil) { 78 | objc_sync_enter(self) 79 | self.error = error 80 | if !isOpen { 81 | isOpen = true 82 | semaphore.signal() 83 | } 84 | key = nil 85 | objc_sync_exit(self) 86 | } 87 | 88 | /// Reset the condition to the closed state using the provided key. 89 | public func close(key: ResultLockKey) { 90 | self.key = key 91 | close() 92 | } 93 | 94 | /// Reset the condition to the closed state. 95 | public func close() { 96 | objc_sync_enter(self) 97 | error = nil 98 | semaphore = DispatchSemaphore(value: 0) 99 | isOpen = false 100 | objc_sync_exit(self) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj/ --------------------------------------------------------------------------------