├── .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/
--------------------------------------------------------------------------------