├── Alerts.jpeg ├── Features.jpeg ├── README.md ├── OrangeLink-DL-Workspace-and-Patch.sh └── OL-patch-files └── rileylink_ios ├── MinimedKit └── Models │ └── PumpModel.swift ├── RileyLinkBLEKit ├── PeripheralManager.swift ├── RileyLinkDevice.swift └── PeripheralManager+RileyLink.swift ├── RileyLinkKitUI └── RileyLinkDeviceTableViewController.swift └── MinimedKitUI └── RileyLinkMinimedDeviceTableViewController.swift /Alerts.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlucasvt/orangelink-feature-patch/HEAD/Alerts.jpeg -------------------------------------------------------------------------------- /Features.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlucasvt/orangelink-feature-patch/HEAD/Features.jpeg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### How do I get it? 2 | 1. The Loop Patch Features are now incorporated into Loop as of Release 2.2.5 (Aug 22 2021) Update to the latest version of Loop. 3 | https://github.com/LoopKit/Loop/releases/ 4 | 5 | 6 | ### What does this patch do? 7 | * Adds Battery Level Status Display (%) 8 | * Adds OrangeLink Firmware (FW) and Hardware (HW) Version Listing 9 | * Adds Battery Voltage (volts) 10 | * Adds % Setting and Alert for Low Battery (Off or Set Point) 11 | * Adds Voltage Setting and Alert for Low Voltage Alert (Off or Set Point) 12 | * Adds Toggle to Enable/Disable Connection State 10 Second Blinking LED 13 | * Adds Toggle to Enable/Disable Connection State Disconnect Vibration Alert 14 | * Adds Test Switches to test Yellow and Red LED’s, and the Haptic Motor 15 | * Adds [Find Device] Command (OrangeLink Pro Only) 16 | * Fix for EMALink Communications Error when using Feature Patch 17 | * Disables MySentry Packets to increase OrangeLink battery life with Medtronic x23/x54 pumps 18 | 19 | ![Features](https://github.com/jlucasvt/orangelink-feature-patch/raw/main/Features.jpeg?raw=true) 20 | ![Alerts](https://github.com/jlucasvt/orangelink-feature-patch/raw/main/Alerts.jpeg?raw=true) 21 | -------------------------------------------------------------------------------- /OrangeLink-DL-Workspace-and-Patch.sh: -------------------------------------------------------------------------------- 1 | # Launch this script by pasting this command into a black terminal window. 2 | # /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/jlucasvt/orangelink-feature-patch/main/OrangeLink-DL-Workspace-and-Patch.sh)" 3 | # Credits - Jeremy Lucas , Vic Wu, LoopDocs Workspace, Loop-N-Learn, Loop Community 4 | 5 | echo "Select A Workspace Build.." 6 | echo "[ 1 ] Loop Master" 7 | echo "[ 2 ] Loop Auto Bolus" 8 | echo "[ 3 ] FreeAPS" 9 | echo "[ 4 ] Download and patch inside a current workspace root folder" 10 | read -p 'Enter the Number (1,2,3,4)?' buildtype 11 | 12 | case $buildtype in 13 | 14 | 1) 15 | # LoopMaster Workspace Build 16 | echo Set environment variables 17 | LOOP_BUILD=$(date +'%y%m%d-%H%M') 18 | LOOP_DIR=~/Downloads/BuildLoop/LoopMaster-$LOOP_BUILD 19 | echo make directories using format year month date hour minute so it can be easily sorted 20 | mkdir ~/Downloads/BuildLoop/ 21 | mkdir $LOOP_DIR 22 | cd $LOOP_DIR 23 | pwd 24 | echo "Download Loop Master Workspace from github" 25 | git clone --branch=master --recurse-submodules https://github.com/LoopKit/LoopWorkspace 26 | cd LoopWorkspace 27 | git remote -v 28 | ;; 29 | 30 | 2) 31 | # Loop AutoBolus Workspace Build 32 | echo Set environment variables 33 | LOOP_BUILD=$(date +'%y%m%d-%H%M') 34 | LOOP_DIR=~/Downloads/BuildLoop/LoopAB-$LOOP_BUILD 35 | echo make directories using format year month date hour minute so it can be easily sorted 36 | mkdir ~/Downloads/BuildLoop/ 37 | mkdir $LOOP_DIR 38 | cd $LOOP_DIR 39 | pwd 40 | echo "Download Loop AutoBolus Workspace from github" 41 | git clone --branch=automatic-bolus --recurse-submodules https://github.com/LoopKit/LoopWorkspace 42 | cd LoopWorkspace 43 | git remote -v 44 | ;; 45 | 46 | 3) 47 | # FreeAPS Workspace Build 48 | echo Set environment variables 49 | LOOP_BUILD=$(date +'%y%m%d-%H%M') 50 | LOOP_DIR=~/Downloads/BuildLoop/FreeAPS-$LOOP_BUILD 51 | echo make directories using format year month date hour minute so it can be easily sorted 52 | mkdir ~/Downloads/BuildLoop/ 53 | mkdir $LOOP_DIR 54 | cd $LOOP_DIR 55 | pwd 56 | echo "Download FreeAPS Workspace from github" 57 | git clone --branch=freeaps --recurse-submodules https://github.com/ivalkou/LoopWorkspace 58 | cd LoopWorkspace 59 | git remote -v 60 | ;; 61 | 62 | *) 63 | echo "Downloading the patch files into exsiting Workspace folder.." 64 | 65 | if [ -d rileylink_ios ] 66 | then 67 | echo "Looks like your in a Workspace root or somewhere that we can patch..." 68 | LOOP_DIR = pwd 69 | else 70 | echo "The Current Directory you are running this script from DOES NOT have a rileylink_ios SubFolder" 71 | echo "You need to run this script from INSIDE a Workspace Project root folder that has a rileylink_ios SubFolder" 72 | exit 73 | fi 74 | ;; 75 | esac 76 | 77 | #PATCHING 78 | 79 | echo Download OrangeLink Patch.. 80 | curl "https://codeload.github.com/jlucasvt/orangelink-feature-patch/zip/refs/heads/main" -O -J -L 81 | 82 | echo "Unzip Orangelink Patch.." 83 | unzip "orangelink-feature-patch-main.zip" 84 | 85 | echo "Patching Workspace Folder" 86 | PATCHFILEROOT=orangelink-feature-patch-main/OL-patch-files/rileylink_ios 87 | echo "Patch File Root: " $PATCHFILEROOT 88 | 89 | WORKSPACEPATCHROOT=rileylink_ios 90 | echo "Workspace Patch Root: "$WORKSPACEPATCHROOT 91 | 92 | # echo Replace File PumpModel.swift 93 | cp -v $PATCHFILEROOT/MinimedKit/Models/*.swift $WORKSPACEPATCHROOT/MinimedKit/Models/ 94 | 95 | # echo Replace File RileyLinkMinimedDeviceTableViewController.swift 96 | cp -v $PATCHFILEROOT/MinimedKitUI/*.swift $WORKSPACEPATCHROOT/MinimedKitUI/ 97 | 98 | # echo Replace File PeripheralManager.swift 99 | # echo Replace File PeripheralManager+RileyLink.swift 100 | # echo Replace File RileyLinkDevice.swift 101 | cp -v $PATCHFILEROOT/RileyLinkBLEKit/*.swift $WORKSPACEPATCHROOT/RileyLinkBLEKit/ 102 | 103 | # echo Replace File RileyLinkDeviceTableViewController.swift 104 | cp -v $PATCHFILEROOT/RileyLinkKitUI/*.swift $WORKSPACEPATCHROOT/RileyLinkKitUI/ 105 | 106 | echo "Cleaning Up" 107 | # rm "orangelink-feature-patch-main.zip" 108 | # rm -r "orangelink-feature-patch-main" 109 | 110 | echo "Open XCode" 111 | xed . 112 | echo "YOU SHOULD CLOSE THIS WINDOW NOW AND FINISH SIGNING TARGETS AND CONFIGURING THE WORKSPACE IN XCODE" 113 | exit 114 | -------------------------------------------------------------------------------- /OL-patch-files/rileylink_ios/MinimedKit/Models/PumpModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PumpModel.swift 3 | // RileyLink 4 | // 5 | // Created by Pete Schwamb on 3/7/16. 6 | // Copyright © 2016 Pete Schwamb. All rights reserved. 7 | // 8 | 9 | 10 | /// Represents a pump model and its defining characteristics. 11 | /// This class implements the `RawRepresentable` protocol 12 | public enum PumpModel: String { 13 | case model508 = "508" 14 | case model511 = "511" 15 | case model711 = "711" 16 | case model512 = "512" 17 | case model712 = "712" 18 | case model515 = "515" 19 | case model715 = "715" 20 | case model522 = "522" 21 | case model722 = "722" 22 | case model523 = "523" 23 | case model723 = "723" 24 | case model530 = "530" 25 | case model730 = "730" 26 | case model540 = "540" 27 | case model740 = "740" 28 | case model551 = "551" 29 | case model751 = "751" 30 | case model554 = "554" 31 | case model754 = "754" 32 | 33 | private var size: Int { 34 | return Int(rawValue)! / 100 35 | } 36 | 37 | private var generation: Int { 38 | return Int(rawValue)! % 100 39 | } 40 | 41 | /// Identifies pumps that support a major-generation shift in record format, starting with the x23. 42 | /// Mirrors the "larger" flag as defined by decoding-carelink 43 | public var larger: Bool { 44 | return generation >= 23 45 | } 46 | 47 | // On newer pumps, square wave boluses are added to history on start of delivery, and updated in place 48 | // when delivery is finished 49 | public var appendsSquareWaveToHistoryOnStartOfDelivery: Bool { 50 | return generation >= 23 51 | } 52 | 53 | public var hasMySentry: Bool { 54 | //TODO: disable MySentry when using OL return generation >= 23 55 | return false 56 | } 57 | 58 | var hasLowSuspend: Bool { 59 | return generation >= 51 60 | } 61 | 62 | public var recordsBasalProfileStartEvents: Bool { 63 | return generation >= 23 64 | } 65 | 66 | /// Newer models allow higher precision delivery, and have bit packing to accomodate this. 67 | public var insulinBitPackingScale: Int { 68 | return (generation >= 23) ? 40 : 10 69 | } 70 | 71 | /// Pulses per unit is the inverse of the minimum volume of delivery. 72 | public var pulsesPerUnit: Int { 73 | return (generation >= 23) ? 40 : 20 74 | } 75 | 76 | public var reservoirCapacity: Int { 77 | switch size { 78 | case 5: 79 | return 176 80 | case 7: 81 | return 300 82 | default: 83 | fatalError("Unknown reservoir capacity for PumpModel.\(self)") 84 | } 85 | } 86 | 87 | /// Even though this is capped by the system at 250 / 10 U, the message takes a UInt16. 88 | var usesTwoBytesForMaxBolus: Bool { 89 | return generation >= 23 90 | } 91 | 92 | public var supportedBasalRates: [Double] { 93 | if generation >= 23 { 94 | // 0.025 units (for rates between 0.0-0.975 U/h) 95 | let rateGroup1 = ((0...39).map { Double($0) / Double(pulsesPerUnit) }) 96 | // 0.05 units (for rates between 1-9.95 U/h) 97 | let rateGroup2 = ((20...199).map { Double($0) / Double(pulsesPerUnit/2) }) 98 | // 0.1 units (for rates between 10-35 U/h) 99 | let rateGroup3 = ((100...350).map { Double($0) / Double(pulsesPerUnit/4) }) 100 | return rateGroup1 + rateGroup2 + rateGroup3 101 | } else { 102 | // 0.05 units for rates between 0.0-35U/hr 103 | return (0...700).map { Double($0) / Double(pulsesPerUnit) } 104 | } 105 | } 106 | 107 | public var maximumBolusVolume: Int { 108 | return 25 109 | } 110 | 111 | public var maximumBasalRate: Double { 112 | return 35 113 | } 114 | 115 | public var supportedBolusVolumes: [Double] { 116 | if generation >= 23 { 117 | let breakpoints: [Int] = [0,1,10,maximumBolusVolume] 118 | let scales: [Int] = [40,20,10] 119 | let scalingGroups = zip(scales, (zip(breakpoints, breakpoints[1...]).map {($0.0)...$0.1})) 120 | let segments = scalingGroups.map { (scale, range) -> [Double] in 121 | let scaledRanges = ((range.lowerBound*scale+1)...(range.upperBound*scale)) 122 | return scaledRanges.map { Double($0) / Double(scale) } 123 | } 124 | return segments.flatMap { $0 } 125 | } else { 126 | return (1...(maximumBolusVolume*10)).map { Double($0) / 10.0 } 127 | } 128 | } 129 | 130 | public var maximumBasalScheduleEntryCount: Int { 131 | return 48 132 | } 133 | 134 | public var minimumBasalScheduleEntryDuration: TimeInterval { 135 | return .minutes(30) 136 | } 137 | 138 | public var isDeliveryRateVariable: Bool { 139 | return generation >= 23 140 | } 141 | 142 | public func bolusDeliveryTime(units: Double) -> TimeInterval { 143 | let unitsPerMinute: Double 144 | if isDeliveryRateVariable { 145 | switch units { 146 | case let u where u < 1.0: 147 | unitsPerMinute = 0.75 148 | case let u where u > 7.5: 149 | unitsPerMinute = units / 5 150 | default: 151 | unitsPerMinute = 1.5 152 | } 153 | } else { 154 | unitsPerMinute = 1.5 155 | } 156 | return TimeInterval(minutes: units / unitsPerMinute) 157 | } 158 | 159 | public func estimateTempBasalProgress(unitsPerHour: Double, duration: TimeInterval, elapsed: TimeInterval) -> (deliveredUnits: Double, progress: Double) { 160 | let roundedVolume = round(unitsPerHour * elapsed.hours * Double(pulsesPerUnit)) / Double(pulsesPerUnit) 161 | return (deliveredUnits: roundedVolume, progress: min(elapsed / duration, 1)) 162 | } 163 | 164 | public func estimateBolusProgress(elapsed: TimeInterval, programmedUnits: Double) -> (deliveredUnits: Double, progress: Double) { 165 | let duration = bolusDeliveryTime(units: programmedUnits) 166 | let timeProgress = min(elapsed / duration, 1) 167 | 168 | let updateResolution: Double 169 | let unroundedVolume: Double 170 | 171 | if isDeliveryRateVariable { 172 | if programmedUnits < 1 { 173 | updateResolution = 40 // Resolution = 0.025 174 | unroundedVolume = timeProgress * programmedUnits 175 | } else { 176 | var remainingUnits = programmedUnits 177 | var baseDuration: TimeInterval = 0 178 | var overlay1Duration: TimeInterval = 0 179 | var overlay2Duration: TimeInterval = 0 180 | let baseDeliveryRate = 1.5 / TimeInterval(minutes: 1) 181 | 182 | baseDuration = min(duration, remainingUnits / baseDeliveryRate) 183 | remainingUnits -= baseDuration * baseDeliveryRate 184 | 185 | overlay1Duration = min(duration, remainingUnits / baseDeliveryRate) 186 | remainingUnits -= overlay1Duration * baseDeliveryRate 187 | 188 | overlay2Duration = min(duration, remainingUnits / baseDeliveryRate) 189 | remainingUnits -= overlay2Duration * baseDeliveryRate 190 | 191 | unroundedVolume = (min(elapsed, baseDuration) + min(elapsed, overlay1Duration) + min(elapsed, overlay2Duration)) * baseDeliveryRate 192 | 193 | if overlay1Duration > elapsed { 194 | updateResolution = 10 // Resolution = 0.1 195 | } else { 196 | updateResolution = 20 // Resolution = 0.05 197 | } 198 | } 199 | 200 | } else { 201 | updateResolution = 20 // Resolution = 0.05 202 | unroundedVolume = timeProgress * programmedUnits 203 | } 204 | let roundedVolume = round(unroundedVolume * updateResolution) / updateResolution 205 | return (deliveredUnits: roundedVolume, progress: roundedVolume / programmedUnits) 206 | } 207 | } 208 | 209 | 210 | extension PumpModel: CustomStringConvertible { 211 | public var description: String { 212 | return rawValue 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /OL-patch-files/rileylink_ios/RileyLinkBLEKit/PeripheralManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralManager.swift 3 | // xDripG5 4 | // 5 | // Copyright © 2017 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import CoreBluetooth 9 | import Foundation 10 | import os.log 11 | 12 | 13 | class PeripheralManager: NSObject { 14 | 15 | private let log = OSLog(category: "PeripheralManager") 16 | 17 | /// 18 | /// This is mutable, because CBPeripheral instances can seemingly become invalid, and need to be periodically re-fetched from CBCentralManager 19 | var peripheral: CBPeripheral { 20 | didSet { 21 | guard oldValue !== peripheral else { 22 | return 23 | } 24 | 25 | log.error("Replacing peripheral reference %{public}@ -> %{public}@", oldValue, peripheral) 26 | 27 | oldValue.delegate = nil 28 | peripheral.delegate = self 29 | 30 | queue.sync { 31 | self.needsConfiguration = true 32 | } 33 | } 34 | } 35 | 36 | /// The dispatch queue used to serialize operations on the peripheral 37 | let queue: DispatchQueue 38 | 39 | /// The condition used to signal command completion 40 | private let commandLock = NSCondition() 41 | 42 | /// The required conditions for the operation to complete 43 | private var commandConditions = [CommandCondition]() 44 | 45 | /// Any error surfaced during the active operation 46 | private var commandError: Error? 47 | 48 | private(set) weak var central: CBCentralManager? 49 | 50 | let configuration: Configuration 51 | 52 | // Confined to `queue` 53 | private var needsConfiguration = true 54 | 55 | var logString = "" 56 | 57 | weak var delegate: PeripheralManagerDelegate? { 58 | didSet { 59 | queue.sync { 60 | needsConfiguration = true 61 | } 62 | } 63 | } 64 | 65 | var setDatas: [UInt8] = [0xbb, 0x0c, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 66 | 67 | // Called from RileyLinkDeviceManager.managerQueue 68 | init(peripheral: CBPeripheral, configuration: Configuration, centralManager: CBCentralManager, queue: DispatchQueue) { 69 | self.peripheral = peripheral 70 | self.central = centralManager 71 | self.configuration = configuration 72 | self.queue = queue 73 | 74 | super.init() 75 | 76 | peripheral.delegate = self 77 | 78 | assertConfiguration() 79 | } 80 | } 81 | 82 | 83 | // MARK: - Nested types 84 | extension PeripheralManager { 85 | struct Configuration { 86 | var serviceCharacteristics: [CBUUID: [CBUUID]] = [:] 87 | var notifyingCharacteristics: [CBUUID: [CBUUID]] = [:] 88 | var valueUpdateMacros: [CBUUID: (_ manager: PeripheralManager) -> Void] = [:] 89 | } 90 | 91 | enum CommandCondition { 92 | case notificationStateUpdate(characteristic: CBCharacteristic, enabled: Bool) 93 | case valueUpdate(characteristic: CBCharacteristic, matching: ((Data?) -> Bool)?) 94 | case write(characteristic: CBCharacteristic) 95 | case discoverServices 96 | case discoverCharacteristicsForService(serviceUUID: CBUUID) 97 | } 98 | } 99 | 100 | protocol PeripheralManagerDelegate: AnyObject { 101 | func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) 102 | 103 | func peripheralManager(_ manager: PeripheralManager, didUpdateNotificationStateFor characteristic: CBCharacteristic) 104 | 105 | func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) 106 | 107 | func peripheralManagerDidUpdateName(_ manager: PeripheralManager) 108 | 109 | func completeConfiguration(for manager: PeripheralManager) throws 110 | } 111 | 112 | 113 | // MARK: - Operation sequence management 114 | extension PeripheralManager { 115 | func configureAndRun(_ block: @escaping (_ manager: PeripheralManager) -> Void) -> (() -> Void) { 116 | return { 117 | // TODO: Accessing self might be a race on initialization 118 | if !self.needsConfiguration && self.peripheral.services == nil { 119 | self.log.error("Configured peripheral has no services. Reconfiguring…") 120 | } 121 | 122 | if self.needsConfiguration || self.peripheral.services == nil { 123 | do { 124 | try self.applyConfiguration() 125 | self.log.default("Peripheral configuration completed") 126 | } catch let error { 127 | self.log.error("Error applying peripheral configuration: %@", String(describing: error)) 128 | // Will retry 129 | } 130 | 131 | do { 132 | if let delegate = self.delegate { 133 | try delegate.completeConfiguration(for: self) 134 | self.log.default("Delegate configuration completed") 135 | self.needsConfiguration = false 136 | } else { 137 | self.log.error("No delegate set for configuration") 138 | } 139 | } catch let error { 140 | self.log.error("Error applying delegate configuration: %@", String(describing: error)) 141 | // Will retry 142 | } 143 | } 144 | 145 | block(self) 146 | } 147 | } 148 | 149 | func perform(_ block: @escaping (_ manager: PeripheralManager) -> Void) { 150 | queue.async(execute: configureAndRun(block)) 151 | } 152 | 153 | private func assertConfiguration() { 154 | perform { (_) in 155 | // Intentionally empty to trigger configuration if necessary 156 | } 157 | } 158 | 159 | private func applyConfiguration(discoveryTimeout: TimeInterval = 2) throws { 160 | try discoverServices(configuration.serviceCharacteristics.keys.map { $0 }, timeout: discoveryTimeout) 161 | 162 | for service in peripheral.services ?? [] { 163 | guard let characteristics = configuration.serviceCharacteristics[service.uuid] else { 164 | // Not all services may have characteristics 165 | continue 166 | } 167 | 168 | try discoverCharacteristics(characteristics, for: service, timeout: discoveryTimeout) 169 | } 170 | 171 | for (serviceUUID, characteristicUUIDs) in configuration.notifyingCharacteristics { 172 | guard let service = peripheral.services?.itemWithUUID(serviceUUID) else { 173 | throw PeripheralManagerError.unknownCharacteristic 174 | continue 175 | } 176 | 177 | add(log: "serviceUUID: \(serviceUUID.uuidString)") 178 | 179 | for characteristicUUID in characteristicUUIDs { 180 | add(log: "characteristicUUID: \(characteristicUUID.uuidString)") 181 | guard let characteristic = service.characteristics?.itemWithUUID(characteristicUUID) else { 182 | throw PeripheralManagerError.unknownCharacteristic 183 | continue 184 | } 185 | 186 | guard !characteristic.isNotifying else { 187 | continue 188 | } 189 | 190 | try setNotifyValue(true, for: characteristic, timeout: discoveryTimeout) 191 | } 192 | } 193 | } 194 | } 195 | 196 | 197 | // MARK: - Synchronous Commands 198 | extension PeripheralManager { 199 | /// - Throws: PeripheralManagerError 200 | func runCommand(timeout: TimeInterval, command: () -> Void) throws { 201 | // Prelude 202 | dispatchPrecondition(condition: .onQueue(queue)) 203 | guard central?.state == .poweredOn && peripheral.state == .connected else { 204 | throw PeripheralManagerError.notReady 205 | } 206 | 207 | commandLock.lock() 208 | 209 | defer { 210 | commandLock.unlock() 211 | } 212 | 213 | guard commandConditions.isEmpty else { 214 | throw PeripheralManagerError.notReady 215 | } 216 | 217 | // Run 218 | command() 219 | 220 | guard !commandConditions.isEmpty else { 221 | // If the command didn't add any conditions, then finish immediately 222 | return 223 | } 224 | 225 | // Postlude 226 | let signaled = commandLock.wait(until: Date(timeIntervalSinceNow: timeout)) 227 | 228 | defer { 229 | commandError = nil 230 | commandConditions = [] 231 | } 232 | 233 | guard signaled else { 234 | throw PeripheralManagerError.timeout 235 | } 236 | 237 | if let error = commandError { 238 | throw PeripheralManagerError.cbPeripheralError(error) 239 | } 240 | } 241 | 242 | /// It's illegal to call this without first acquiring the commandLock 243 | /// 244 | /// - Parameter condition: The condition to add 245 | func addCondition(_ condition: CommandCondition) { 246 | dispatchPrecondition(condition: .onQueue(queue)) 247 | commandConditions.append(condition) 248 | } 249 | 250 | func discoverServices(_ serviceUUIDs: [CBUUID], timeout: TimeInterval) throws { 251 | let servicesToDiscover = peripheral.servicesToDiscover(from: serviceUUIDs) 252 | 253 | guard servicesToDiscover.count > 0 else { 254 | return 255 | } 256 | 257 | try runCommand(timeout: timeout) { 258 | addCondition(.discoverServices) 259 | 260 | peripheral.discoverServices(serviceUUIDs) 261 | } 262 | } 263 | 264 | func discoverCharacteristics(_ characteristicUUIDs: [CBUUID], for service: CBService, timeout: TimeInterval) throws { 265 | let characteristicsToDiscover = peripheral.characteristicsToDiscover(from: characteristicUUIDs, for: service) 266 | 267 | guard characteristicsToDiscover.count > 0 else { 268 | return 269 | } 270 | 271 | try runCommand(timeout: timeout) { 272 | addCondition(.discoverCharacteristicsForService(serviceUUID: service.uuid)) 273 | 274 | peripheral.discoverCharacteristics(characteristicsToDiscover, for: service) 275 | } 276 | } 277 | 278 | /// - Throws: PeripheralManagerError 279 | func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic, timeout: TimeInterval) throws { 280 | add(log: "setNotifyValue: \(characteristic.uuid.uuidString)") 281 | try runCommand(timeout: timeout) { 282 | addCondition(.notificationStateUpdate(characteristic: characteristic, enabled: enabled)) 283 | 284 | peripheral.setNotifyValue(enabled, for: characteristic) 285 | } 286 | } 287 | 288 | /// - Throws: PeripheralManagerError 289 | func readValue(for characteristic: CBCharacteristic, timeout: TimeInterval) throws -> Data? { 290 | try runCommand(timeout: timeout) { 291 | addCondition(.valueUpdate(characteristic: characteristic, matching: nil)) 292 | 293 | peripheral.readValue(for: characteristic) 294 | } 295 | 296 | return characteristic.value 297 | } 298 | 299 | /// - Throws: PeripheralManagerError 300 | func wait(for characteristic: CBCharacteristic, timeout: TimeInterval) throws -> Data { 301 | try runCommand(timeout: timeout) { 302 | addCondition(.valueUpdate(characteristic: characteristic, matching: nil)) 303 | } 304 | 305 | guard let value = characteristic.value else { 306 | throw PeripheralManagerError.timeout 307 | } 308 | 309 | return value 310 | } 311 | 312 | /// - Throws: PeripheralManagerError 313 | func writeValue(_ value: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, timeout: TimeInterval) throws { 314 | try runCommand(timeout: timeout) { 315 | if case .withResponse = type { 316 | addCondition(.write(characteristic: characteristic)) 317 | } 318 | 319 | peripheral.writeValue(value, for: characteristic, type: type) 320 | } 321 | } 322 | } 323 | 324 | 325 | // MARK: - Delegate methods executed on the central's queue 326 | extension PeripheralManager: CBPeripheralDelegate { 327 | 328 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 329 | commandLock.lock() 330 | 331 | if let index = commandConditions.firstIndex(where: { (condition) -> Bool in 332 | if case .discoverServices = condition { 333 | return true 334 | } else { 335 | return false 336 | } 337 | }) { 338 | commandConditions.remove(at: index) 339 | commandError = error 340 | 341 | if commandConditions.isEmpty { 342 | commandLock.broadcast() 343 | } 344 | } 345 | 346 | commandLock.unlock() 347 | } 348 | 349 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 350 | commandLock.lock() 351 | 352 | if let index = commandConditions.firstIndex(where: { (condition) -> Bool in 353 | if case .discoverCharacteristicsForService(serviceUUID: service.uuid) = condition { 354 | return true 355 | } else { 356 | return false 357 | } 358 | }) { 359 | commandConditions.remove(at: index) 360 | commandError = error 361 | 362 | if commandConditions.isEmpty { 363 | commandLock.broadcast() 364 | } 365 | } 366 | 367 | commandLock.unlock() 368 | } 369 | 370 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { 371 | commandLock.lock() 372 | 373 | if let index = commandConditions.firstIndex(where: { (condition) -> Bool in 374 | if case .notificationStateUpdate(characteristic: characteristic, enabled: characteristic.isNotifying) = condition { 375 | return true 376 | } else { 377 | return false 378 | } 379 | }) { 380 | commandConditions.remove(at: index) 381 | commandError = error 382 | 383 | if commandConditions.isEmpty { 384 | commandLock.broadcast() 385 | } 386 | } 387 | 388 | commandLock.unlock() 389 | delegate?.peripheralManager(self, didUpdateNotificationStateFor: characteristic) 390 | } 391 | 392 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { 393 | commandLock.lock() 394 | 395 | if let index = commandConditions.firstIndex(where: { (condition) -> Bool in 396 | if case .write(characteristic: characteristic) = condition { 397 | return true 398 | } else { 399 | return false 400 | } 401 | }) { 402 | commandConditions.remove(at: index) 403 | commandError = error 404 | 405 | if commandConditions.isEmpty { 406 | commandLock.broadcast() 407 | } 408 | } 409 | 410 | commandLock.unlock() 411 | } 412 | 413 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 414 | commandLock.lock() 415 | 416 | if let error = error { 417 | add(log: error.localizedDescription) 418 | } 419 | 420 | if let value = characteristic.value { 421 | add(log: value.hexadecimalString) 422 | } 423 | 424 | var notifyDelegate = false 425 | 426 | if let index = commandConditions.firstIndex(where: { (condition) -> Bool in 427 | if case .valueUpdate(characteristic: characteristic, matching: let matching) = condition { 428 | return matching?(characteristic.value) ?? true 429 | } else { 430 | return false 431 | } 432 | }) { 433 | commandConditions.remove(at: index) 434 | commandError = error 435 | 436 | if commandConditions.isEmpty { 437 | commandLock.broadcast() 438 | } 439 | } else if let macro = configuration.valueUpdateMacros[characteristic.uuid] { 440 | macro(self) 441 | } else { 442 | notifyDelegate = true // execute after the unlock 443 | } 444 | 445 | commandLock.unlock() 446 | 447 | if notifyDelegate { 448 | // If we weren't expecting this notification, pass it along to the delegate 449 | delegate?.peripheralManager(self, didUpdateValueFor: characteristic) 450 | } 451 | } 452 | 453 | func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { 454 | delegate?.peripheralManager(self, didReadRSSI: RSSI, error: error) 455 | } 456 | 457 | func peripheralDidUpdateName(_ peripheral: CBPeripheral) { 458 | delegate?.peripheralManagerDidUpdateName(self) 459 | } 460 | } 461 | 462 | 463 | extension PeripheralManager: CBCentralManagerDelegate { 464 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 465 | switch central.state { 466 | case .poweredOn: 467 | assertConfiguration() 468 | default: 469 | break 470 | } 471 | } 472 | 473 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 474 | switch peripheral.state { 475 | case .connected: 476 | assertConfiguration() 477 | default: 478 | break 479 | } 480 | } 481 | } 482 | 483 | extension PeripheralManager { 484 | 485 | func add(log: String) { 486 | print("[log]: \(log)") 487 | logString += "\(Date())\n\(log)\n" 488 | if logString.count > 10000 { 489 | logString.removeFirst(1000) 490 | } 491 | } 492 | 493 | public override var debugDescription: String { 494 | var items = [ 495 | "## PeripheralManager", 496 | "peripheral: \(peripheral)", 497 | "log: \(logString)" 498 | ] 499 | queue.sync { 500 | items.append("needsConfiguration: \(needsConfiguration)") 501 | } 502 | return items.joined(separator: "\n") 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /OL-patch-files/rileylink_ios/RileyLinkBLEKit/RileyLinkDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RileyLinkDevice.swift 3 | // RileyLinkBLEKit 4 | // 5 | // Copyright © 2017 Pete Schwamb. All rights reserved. 6 | // 7 | 8 | import CoreBluetooth 9 | import os.log 10 | 11 | 12 | /// TODO: Should we be tracking the most recent "pump" RSSI? 13 | public class RileyLinkDevice { 14 | let manager: PeripheralManager 15 | 16 | private let log = OSLog(category: "RileyLinkDevice") 17 | 18 | // Confined to `manager.queue` 19 | private var bleFirmwareVersion: BLEFirmwareVersion? 20 | 21 | // Confined to `manager.queue` 22 | private var radioFirmwareVersion: RadioFirmwareVersion? 23 | 24 | // Confined to `lock` 25 | private var idleListeningState: IdleListeningState = .disabled 26 | 27 | // Confined to `lock` 28 | private var lastIdle: Date? 29 | 30 | // Confined to `lock` 31 | // TODO: Tidy up this state/preference machine 32 | private var isIdleListeningPending = false 33 | 34 | // Confined to `lock` 35 | private var isTimerTickEnabled = true 36 | 37 | // Confined to `lock` 38 | private var logs = "" 39 | 40 | /// Serializes access to device state 41 | private var lock = os_unfair_lock() 42 | 43 | private var fw_hw = "FW/HW" 44 | public var ledOn: Bool = false 45 | public var vibrationOn: Bool = false 46 | public var voltage = "" 47 | 48 | /// The queue used to serialize sessions and observe when they've drained 49 | private let sessionQueue: OperationQueue = { 50 | let queue = OperationQueue() 51 | queue.name = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.sessionQueue" 52 | queue.maxConcurrentOperationCount = 1 53 | 54 | return queue 55 | }() 56 | 57 | private var sessionQueueOperationCountObserver: NSKeyValueObservation! 58 | 59 | init(peripheralManager: PeripheralManager) { 60 | self.manager = peripheralManager 61 | sessionQueue.underlyingQueue = peripheralManager.queue 62 | 63 | peripheralManager.delegate = self 64 | 65 | sessionQueueOperationCountObserver = sessionQueue.observe(\.operationCount, options: [.new]) { [weak self] (queue, change) in 66 | if let newValue = change.newValue, newValue == 0 { 67 | self?.log.debug("Session queue operation count is now empty") 68 | self?.assertIdleListening(forceRestart: true) 69 | } 70 | } 71 | } 72 | } 73 | 74 | 75 | // MARK: - Peripheral operations. Thread-safe. 76 | extension RileyLinkDevice { 77 | public var name: String? { 78 | return manager.peripheral.name 79 | } 80 | 81 | public var isOrangePro: Bool { 82 | //TODO: Determine Orange vs Riley 83 | return true 84 | } 85 | 86 | public var deviceURI: String { 87 | return "rileylink://\(name ?? peripheralIdentifier.uuidString)" 88 | } 89 | 90 | public var peripheralIdentifier: UUID { 91 | return manager.peripheral.identifier 92 | } 93 | 94 | public var peripheralState: CBPeripheralState { 95 | return manager.peripheral.state 96 | } 97 | 98 | public func readRSSI() { 99 | guard case .connected = manager.peripheral.state, case .poweredOn? = manager.central?.state else { 100 | return 101 | } 102 | manager.peripheral.readRSSI() 103 | } 104 | 105 | public func setCustomName(_ name: String) { 106 | manager.setCustomName(name) 107 | } 108 | 109 | public func getBatterylevel() -> String { 110 | do { 111 | return try manager.readBatteryLevel(timeout: 1) 112 | } catch {} 113 | return "" 114 | } 115 | 116 | public func orangeAction(mode: Int) { 117 | add(log: "orangeAction: \(mode)") 118 | manager.orangeAction(mode: RileyLinkOrangeMode(rawValue: UInt8(mode))!) 119 | } 120 | 121 | public func orangeSetAction(index: Int, open: Bool) { 122 | add(log: "orangeSetAction: \(index), \(open)") 123 | manager.setAction(index: index, open: open) 124 | } 125 | 126 | public func orangeWritePwd() { 127 | add(log: "orangeWritePwd") 128 | manager.orangeWritePwd() 129 | } 130 | 131 | public func orangeClose() { 132 | add(log: "orangeClose") 133 | manager.orangeClose() 134 | } 135 | 136 | public func orangeReadSet() { 137 | add(log: "orangeReadSet") 138 | manager.orangeReadSet() 139 | } 140 | 141 | public func orangeReadVDC() { 142 | add(log: "orangeReadVDC") 143 | manager.orangeReadVDC() 144 | } 145 | 146 | public func findDevices() { 147 | add(log: "findDevices") 148 | manager.findDevices() 149 | } 150 | 151 | public func enableBLELEDs() { 152 | manager.setLEDMode(mode: .on) 153 | } 154 | 155 | /// Asserts that the caller is currently on the session queue 156 | public func assertOnSessionQueue() { 157 | dispatchPrecondition(condition: .onQueue(manager.queue)) 158 | } 159 | 160 | /// Schedules a closure to execute on the session queue after a specified time 161 | /// 162 | /// - Parameters: 163 | /// - deadline: The time after which to execute 164 | /// - execute: The closure to execute 165 | public func sessionQueueAsyncAfter(deadline: DispatchTime, execute: @escaping () -> Void) { 166 | manager.queue.asyncAfter(deadline: deadline, execute: execute) 167 | } 168 | } 169 | 170 | 171 | extension RileyLinkDevice: Equatable, Hashable { 172 | public static func ==(lhs: RileyLinkDevice, rhs: RileyLinkDevice) -> Bool { 173 | return lhs === rhs 174 | } 175 | 176 | public func hash(into hasher: inout Hasher) { 177 | hasher.combine(peripheralIdentifier) 178 | } 179 | } 180 | 181 | 182 | // MARK: - Status management 183 | extension RileyLinkDevice { 184 | public struct Status { 185 | public let lastIdle: Date? 186 | 187 | public let name: String? 188 | 189 | public let bleFirmwareVersion: BLEFirmwareVersion? 190 | 191 | public let radioFirmwareVersion: RadioFirmwareVersion? 192 | 193 | public let fw_hw: String? 194 | 195 | public var ledOn: Bool = false 196 | public var vibrationOn: Bool = false 197 | public var voltage = "" 198 | } 199 | 200 | public func getStatus(_ completion: @escaping (_ status: Status) -> Void) { 201 | os_unfair_lock_lock(&lock) 202 | let lastIdle = self.lastIdle 203 | os_unfair_lock_unlock(&lock) 204 | 205 | self.manager.queue.async { 206 | completion(Status( 207 | lastIdle: lastIdle, 208 | name: self.name, 209 | bleFirmwareVersion: self.bleFirmwareVersion, 210 | radioFirmwareVersion: self.radioFirmwareVersion, 211 | fw_hw: self.fw_hw, 212 | ledOn: self.ledOn, 213 | vibrationOn: self.vibrationOn, 214 | voltage: self.voltage 215 | )) 216 | } 217 | } 218 | } 219 | 220 | 221 | // MARK: - Command session management 222 | extension RileyLinkDevice { 223 | public func runSession(withName name: String, _ block: @escaping (_ session: CommandSession) -> Void) { 224 | sessionQueue.addOperation(manager.configureAndRun({ [weak self] (manager) in 225 | self?.log.default("======================== %{public}@ ===========================", name) 226 | let bleFirmwareVersion = self?.bleFirmwareVersion 227 | let radioFirmwareVersion = self?.radioFirmwareVersion 228 | 229 | if bleFirmwareVersion == nil || radioFirmwareVersion == nil { 230 | self?.log.error("Running session with incomplete configuration: bleFirmwareVersion %{public}@, radioFirmwareVersion: %{public}@", String(describing: bleFirmwareVersion), String(describing: radioFirmwareVersion)) 231 | } 232 | 233 | block(CommandSession(manager: manager, responseType: bleFirmwareVersion?.responseType ?? .buffered, firmwareVersion: radioFirmwareVersion ?? .unknown)) 234 | self?.log.default("------------------------ %{public}@ ---------------------------", name) 235 | })) 236 | } 237 | } 238 | 239 | 240 | // MARK: - Idle management 241 | extension RileyLinkDevice { 242 | public enum IdleListeningState { 243 | case enabled(timeout: TimeInterval, channel: UInt8) 244 | case disabled 245 | } 246 | 247 | func setIdleListeningState(_ state: IdleListeningState) { 248 | os_unfair_lock_lock(&lock) 249 | let oldValue = idleListeningState 250 | idleListeningState = state 251 | os_unfair_lock_unlock(&lock) 252 | 253 | switch (oldValue, state) { 254 | case (.disabled, .enabled): 255 | assertIdleListening(forceRestart: true) 256 | case (.enabled, .enabled): 257 | assertIdleListening(forceRestart: false) 258 | default: 259 | break 260 | } 261 | } 262 | 263 | public func assertIdleListening(forceRestart: Bool = false) { 264 | os_unfair_lock_lock(&lock) 265 | guard case .enabled(timeout: let timeout, channel: let channel) = self.idleListeningState else { 266 | os_unfair_lock_unlock(&lock) 267 | return 268 | } 269 | 270 | guard case .connected = self.manager.peripheral.state, case .poweredOn? = self.manager.central?.state else { 271 | os_unfair_lock_unlock(&lock) 272 | return 273 | } 274 | 275 | guard forceRestart || (self.lastIdle ?? .distantPast).timeIntervalSinceNow < -timeout else { 276 | os_unfair_lock_unlock(&lock) 277 | return 278 | } 279 | 280 | guard !self.isIdleListeningPending else { 281 | os_unfair_lock_unlock(&lock) 282 | return 283 | } 284 | 285 | self.isIdleListeningPending = true 286 | os_unfair_lock_unlock(&lock) 287 | self.log.debug("Enqueuing idle listening") 288 | 289 | self.manager.startIdleListening(idleTimeout: timeout, channel: channel) { (error) in 290 | os_unfair_lock_lock(&self.lock) 291 | self.isIdleListeningPending = false 292 | 293 | if let error = error { 294 | self.log.error("Unable to start idle listening: %@", String(describing: error)) 295 | os_unfair_lock_unlock(&self.lock) 296 | } else { 297 | self.lastIdle = Date() 298 | os_unfair_lock_unlock(&self.lock) 299 | NotificationCenter.default.post(name: .DeviceDidStartIdle, object: self) 300 | } 301 | } 302 | } 303 | } 304 | 305 | 306 | // MARK: - Timer tick management 307 | extension RileyLinkDevice { 308 | func setTimerTickEnabled(_ enabled: Bool) { 309 | os_unfair_lock_lock(&lock) 310 | self.isTimerTickEnabled = enabled 311 | os_unfair_lock_unlock(&lock) 312 | self.assertTimerTick() 313 | } 314 | 315 | func assertTimerTick() { 316 | os_unfair_lock_lock(&self.lock) 317 | let isTimerTickEnabled = self.isTimerTickEnabled 318 | os_unfair_lock_unlock(&self.lock) 319 | 320 | if isTimerTickEnabled != self.manager.timerTickEnabled { 321 | self.manager.setTimerTickEnabled(isTimerTickEnabled) 322 | } 323 | } 324 | } 325 | 326 | 327 | // MARK: - CBCentralManagerDelegate Proxying 328 | extension RileyLinkDevice { 329 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 330 | if case .poweredOn = central.state { 331 | assertIdleListening(forceRestart: false) 332 | assertTimerTick() 333 | } 334 | 335 | manager.centralManagerDidUpdateState(central) 336 | } 337 | 338 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 339 | log.debug("didConnect %@", peripheral) 340 | if case .connected = peripheral.state { 341 | assertIdleListening(forceRestart: false) 342 | assertTimerTick() 343 | } 344 | 345 | manager.centralManager(central, didConnect: peripheral) 346 | NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self) 347 | } 348 | 349 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 350 | log.debug("didDisconnectPeripheral %@", peripheral) 351 | NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self) 352 | } 353 | 354 | func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 355 | log.debug("didFailToConnect %@", peripheral) 356 | NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self) 357 | } 358 | } 359 | 360 | 361 | extension RileyLinkDevice: PeripheralManagerDelegate { 362 | func peripheralManager(_ manager: PeripheralManager, didUpdateNotificationStateFor characteristic: CBCharacteristic) { 363 | add(log: "didUpdate: \(characteristic.uuid.uuidString)") 364 | // switch OrangeServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) { 365 | // case .orange, .orangeNotif: 366 | // manager.writePsw = true 367 | // orangeWritePwd() 368 | // default: 369 | // break 370 | // } 371 | log.debug("Did didUpdateNotificationStateFor %@", characteristic) 372 | } 373 | 374 | // This is called from the central's queue 375 | func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) { 376 | log.debug("Did UpdateValueFor %@", characteristic) 377 | add(log: "Did UpdateValueFor: \(characteristic.uuid.uuidString), value: \(characteristic.value?.hexadecimalString ?? "")") 378 | switch MainServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) { 379 | case .data?: 380 | guard let value = characteristic.value, value.count > 0 else { 381 | return 382 | } 383 | 384 | self.manager.queue.async { 385 | if let responseType = self.bleFirmwareVersion?.responseType { 386 | let response: PacketResponse? 387 | 388 | switch responseType { 389 | case .buffered: 390 | var buffer = ResponseBuffer(endMarker: 0x00) 391 | buffer.append(value) 392 | response = buffer.responses.last 393 | case .single: 394 | response = PacketResponse(data: value) 395 | } 396 | 397 | if let response = response { 398 | switch response.code { 399 | case .rxTimeout, .commandInterrupted, .zeroData, .invalidParam, .unknownCommand: 400 | self.log.debug("Idle error received: %@", String(describing: response.code)) 401 | case .success: 402 | if let packet = response.packet { 403 | self.log.debug("Idle packet received: %@", value.hexadecimalString) 404 | NotificationCenter.default.post( 405 | name: .DevicePacketReceived, 406 | object: self, 407 | userInfo: [RileyLinkDevice.notificationPacketKey: packet] 408 | ) 409 | } 410 | } 411 | } else { 412 | self.log.error("Unknown idle response: %@", value.hexadecimalString) 413 | } 414 | } else { 415 | self.log.error("Skipping parsing characteristic value update due to missing BLE firmware version") 416 | } 417 | 418 | self.resetBatteryAlert() 419 | self.orangeReadVDC() 420 | self.assertIdleListening(forceRestart: true) 421 | } 422 | case .responseCount?: 423 | // PeripheralManager.Configuration.valueUpdateMacros is responsible for handling this response. 424 | self.resetBatteryAlert() 425 | self.orangeReadVDC() 426 | break 427 | case .timerTick?: 428 | NotificationCenter.default.post(name: .DeviceTimerDidTick, object: self) 429 | self.resetBatteryAlert() 430 | self.orangeReadVDC() 431 | assertIdleListening(forceRestart: false) 432 | case .customName?, .firmwareVersion?, .ledMode?, .none: 433 | break 434 | } 435 | 436 | switch OrangeServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) { 437 | case .orange, .orangeNotif: 438 | guard let data = characteristic.value, !data.isEmpty else { return } 439 | if data.first == 0xbb { 440 | guard let data = characteristic.value, data.count > 6 else { return } 441 | if data[1] == 0x09, data[2] == 0xaa { 442 | fw_hw = "FW\(data[3]).\(data[4])/HW\(data[5]).\(data[6])" 443 | NotificationCenter.default.post(name: .DeviceFW_HWChange, object: self) 444 | } 445 | } else if data.first == 0xdd { 446 | guard let data = characteristic.value, data.count > 2 else { return } 447 | if data[1] == 0x01 { 448 | guard let data = characteristic.value, data.count > 5 else { return } 449 | ledOn = (data[3] != 0) 450 | vibrationOn = (data[4] != 0) 451 | NotificationCenter.default.post(name: .DeviceFW_HWChange, object: self) 452 | } else if data[1] == 0x03 { 453 | guard var data = characteristic.value, data.count > 4 else { return } 454 | data = Data(data[3...4]) 455 | let int = UInt16(bigEndian: data.withUnsafeBytes { $0.load(as: UInt16.self) }) 456 | voltage = String(format: "%.1f%", Float(int) / 1000) 457 | NotificationCenter.default.post(name: .DeviceFW_HWChange, object: self) 458 | 459 | guard Date() > Date(timeIntervalSince1970: UserDefaults.standard.double(forKey: "voltage_date")).addingTimeInterval(60 * 60), 460 | UserDefaults.standard.double(forKey: "voltage_alert_value") != 0 else { return } 461 | 462 | UserDefaults.standard.setValue(Date().timeIntervalSince1970, forKey: "voltage_date") 463 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 464 | let value = UserDefaults.standard.double(forKey: "voltage_alert_value") 465 | if (Double(self.voltage) ?? 100) <= value { 466 | let content = UNMutableNotificationContent() 467 | content.title = "Low Voltage" 468 | content.subtitle = self.voltage 469 | let request = UNNotificationRequest.init(identifier: "Orange Low Voltage", content: content, trigger: nil) 470 | UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) 471 | } 472 | } 473 | } 474 | } 475 | default: 476 | break 477 | } 478 | } 479 | 480 | func resetBatteryAlert() { 481 | guard Date() > Date(timeIntervalSince1970: UserDefaults.standard.double(forKey: "battery_date")).addingTimeInterval(60 * 60) else { return } 482 | if UserDefaults.standard.integer(forKey: "battery_alert_value") != 0 { 483 | manager.queue.async { 484 | UserDefaults.standard.setValue(Date().timeIntervalSince1970, forKey: "battery_date") 485 | let batteryLevel = self.getBatterylevel() 486 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 487 | let value = UserDefaults.standard.integer(forKey: "battery_alert_value") 488 | if (Int(batteryLevel) ?? 100) <= value { 489 | let content = UNMutableNotificationContent() 490 | content.title = "Low Battery" 491 | content.subtitle = batteryLevel 492 | let request = UNNotificationRequest.init(identifier: "Orange Low Battery", content: content, trigger: nil) 493 | UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) 494 | } 495 | } 496 | } 497 | } 498 | } 499 | 500 | func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) { 501 | NotificationCenter.default.post( 502 | name: .DeviceRSSIDidChange, 503 | object: self, 504 | userInfo: [RileyLinkDevice.notificationRSSIKey: RSSI] 505 | ) 506 | } 507 | 508 | func peripheralManagerDidUpdateName(_ manager: PeripheralManager) { 509 | NotificationCenter.default.post( 510 | name: .DeviceNameDidChange, 511 | object: self, 512 | userInfo: nil 513 | ) 514 | } 515 | 516 | func completeConfiguration(for manager: PeripheralManager) throws { 517 | // Read bluetooth version to determine compatibility 518 | log.default("Reading firmware versions for PeripheralManager configuration") 519 | let bleVersionString = try manager.readBluetoothFirmwareVersion(timeout: 1) 520 | bleFirmwareVersion = BLEFirmwareVersion(versionString: bleVersionString) 521 | 522 | let radioVersionString = try manager.readRadioFirmwareVersion(timeout: 1, responseType: bleFirmwareVersion?.responseType ?? .buffered) 523 | radioFirmwareVersion = RadioFirmwareVersion(versionString: radioVersionString) 524 | 525 | try manager.setOrangeNotifyOn() 526 | } 527 | } 528 | 529 | 530 | extension RileyLinkDevice: CustomDebugStringConvertible { 531 | 532 | public func add(log: String) { 533 | os_unfair_lock_lock(&lock) 534 | if self.logs.count > 10000 { 535 | self.logs.removeLast(1000) 536 | } 537 | self.logs.append("\(Date())\n\(log)\n") 538 | os_unfair_lock_unlock(&lock) 539 | } 540 | 541 | public var debugDescription: String { 542 | os_unfair_lock_lock(&lock) 543 | let lastIdle = self.lastIdle 544 | let isIdleListeningPending = self.isIdleListeningPending 545 | let isTimerTickEnabled = self.isTimerTickEnabled 546 | os_unfair_lock_unlock(&lock) 547 | 548 | return [ 549 | "## RileyLinkDevice", 550 | "* name: \(name ?? "")", 551 | "* lastIdle: \(lastIdle ?? .distantPast)", 552 | "* isIdleListeningPending: \(isIdleListeningPending)", 553 | "* isTimerTickEnabled: \(isTimerTickEnabled)", 554 | "* isTimerTickNotifying: \(manager.timerTickEnabled)", 555 | "* radioFirmware: \(String(describing: radioFirmwareVersion))", 556 | "* bleFirmware: \(String(describing: bleFirmwareVersion))", 557 | "* peripheralManager: \(manager)", 558 | "* sessionQueue.operationCount: \(sessionQueue.operationCount)", 559 | "* logs: \(logs)", 560 | "* manager logs: \(manager.logString)" 561 | ].joined(separator: "\n") 562 | } 563 | } 564 | 565 | 566 | extension RileyLinkDevice { 567 | public static let notificationPacketKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationPacket" 568 | 569 | public static let notificationRSSIKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationRSSI" 570 | } 571 | 572 | 573 | extension Notification.Name { 574 | public static let DeviceConnectionStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.ConnectionStateDidChange") 575 | 576 | public static let DeviceDidStartIdle = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DidStartIdle") 577 | 578 | public static let DeviceNameDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.NameDidChange") 579 | 580 | public static let DevicePacketReceived = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.PacketReceived") 581 | 582 | public static let DeviceRSSIDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.RSSIDidChange") 583 | 584 | public static let DeviceTimerDidTick = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.TimerTickDidChange") 585 | 586 | public static let DeviceFW_HWChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DeviceFW_HWChange") 587 | } 588 | -------------------------------------------------------------------------------- /OL-patch-files/rileylink_ios/RileyLinkBLEKit/PeripheralManager+RileyLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralManager+RileyLink.swift 3 | // xDripG5 4 | // 5 | // Copyright © 2017 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import CoreBluetooth 9 | import os.log 10 | 11 | 12 | protocol CBUUIDRawValue: RawRepresentable {} 13 | extension CBUUIDRawValue where RawValue == String { 14 | var cbUUID: CBUUID { 15 | return CBUUID(string: rawValue.uppercased()) 16 | } 17 | } 18 | 19 | 20 | enum RileyLinkServiceUUID: String, CBUUIDRawValue { 21 | case main = "0235733B-99C5-4197-B856-69219C2A3845" 22 | case battery = "180F" 23 | case orange = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" 24 | } 25 | 26 | enum MainServiceCharacteristicUUID: String, CBUUIDRawValue { 27 | case data = "C842E849-5028-42E2-867C-016ADADA9155" 28 | case responseCount = "6E6C7910-B89E-43A5-A0FE-50C5E2B81F4A" 29 | case customName = "D93B2AF0-1E28-11E4-8C21-0800200C9A66" 30 | case timerTick = "6E6C7910-B89E-43A5-78AF-50C5E2B86F7E" 31 | case firmwareVersion = "30D99DC9-7C91-4295-A051-0A104D238CF2" 32 | case ledMode = "C6D84241-F1A7-4F9C-A25F-FCE16732F14E" 33 | } 34 | 35 | enum BatteryServiceCharacteristicUUID: String, CBUUIDRawValue { 36 | case battery_level = "2A19" 37 | } 38 | 39 | enum OrangeServiceCharacteristicUUID: String, CBUUIDRawValue { 40 | case orange = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" 41 | case orangeNotif = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" 42 | } 43 | 44 | enum RileyLinkOrangeMode: UInt8 { 45 | case yellow = 0x1 46 | case red = 0x2 47 | case off = 0x3 48 | case shake = 0x4 49 | case shakeOff = 0x5 50 | case fw_hw = 0x9 51 | } 52 | 53 | enum RileyLinkLEDMode: UInt8 { 54 | case off = 0x00 55 | case on = 0x01 56 | case auto = 0x02 57 | } 58 | 59 | 60 | extension PeripheralManager.Configuration { 61 | static var rileyLink: PeripheralManager.Configuration { 62 | return PeripheralManager.Configuration( 63 | serviceCharacteristics: [ 64 | RileyLinkServiceUUID.main.cbUUID: [ 65 | MainServiceCharacteristicUUID.data.cbUUID, 66 | MainServiceCharacteristicUUID.responseCount.cbUUID, 67 | MainServiceCharacteristicUUID.customName.cbUUID, 68 | MainServiceCharacteristicUUID.timerTick.cbUUID, 69 | MainServiceCharacteristicUUID.firmwareVersion.cbUUID, 70 | MainServiceCharacteristicUUID.ledMode.cbUUID 71 | ], 72 | RileyLinkServiceUUID.battery.cbUUID: [ 73 | BatteryServiceCharacteristicUUID.battery_level.cbUUID 74 | ], 75 | RileyLinkServiceUUID.orange.cbUUID: [ 76 | OrangeServiceCharacteristicUUID.orange.cbUUID, 77 | OrangeServiceCharacteristicUUID.orangeNotif.cbUUID, 78 | ] 79 | ], 80 | notifyingCharacteristics: [ 81 | RileyLinkServiceUUID.main.cbUUID: [ 82 | MainServiceCharacteristicUUID.responseCount.cbUUID 83 | ], 84 | RileyLinkServiceUUID.orange.cbUUID: [ 85 | OrangeServiceCharacteristicUUID.orangeNotif.cbUUID, 86 | ] 87 | ], 88 | valueUpdateMacros: [ 89 | // When the responseCount changes, the data characteristic should be read. 90 | MainServiceCharacteristicUUID.responseCount.cbUUID: { (manager: PeripheralManager) in 91 | guard let dataCharacteristic = manager.peripheral.getCharacteristicWithUUID(.data) 92 | else { 93 | return 94 | } 95 | 96 | manager.peripheral.readValue(for: dataCharacteristic) 97 | } 98 | ] 99 | ) 100 | } 101 | } 102 | 103 | fileprivate extension CBPeripheral { 104 | func getBatteryCharacteristic(_ uuid: BatteryServiceCharacteristicUUID, serviceUUID: RileyLinkServiceUUID = .battery) -> CBCharacteristic? { 105 | guard let service = services?.itemWithUUID(serviceUUID.cbUUID) else { 106 | return nil 107 | } 108 | 109 | return service.characteristics?.itemWithUUID(uuid.cbUUID) 110 | } 111 | } 112 | 113 | fileprivate extension CBPeripheral { 114 | func getOrangeCharacteristic(_ uuid: OrangeServiceCharacteristicUUID, serviceUUID: RileyLinkServiceUUID = .orange) -> CBCharacteristic? { 115 | guard let service = services?.itemWithUUID(serviceUUID.cbUUID) else { 116 | return nil 117 | } 118 | 119 | return service.characteristics?.itemWithUUID(uuid.cbUUID) 120 | } 121 | } 122 | 123 | 124 | 125 | fileprivate extension CBPeripheral { 126 | func getCharacteristicWithUUID(_ uuid: MainServiceCharacteristicUUID, serviceUUID: RileyLinkServiceUUID = .main) -> CBCharacteristic? { 127 | guard let service = services?.itemWithUUID(serviceUUID.cbUUID) else { 128 | return nil 129 | } 130 | 131 | return service.characteristics?.itemWithUUID(uuid.cbUUID) 132 | } 133 | } 134 | 135 | 136 | extension CBCentralManager { 137 | func scanForPeripherals(withOptions options: [String: Any]? = nil) { 138 | scanForPeripherals(withServices: [RileyLinkServiceUUID.main.cbUUID], options: options) 139 | } 140 | } 141 | 142 | 143 | extension Command { 144 | /// Encodes a command's data by validating and prepending its length 145 | /// 146 | /// - Returns: Writable command data 147 | /// - Throws: RileyLinkDeviceError.writeSizeLimitExceeded if the command data is too long 148 | fileprivate func writableData() throws -> Data { 149 | var data = self.data 150 | 151 | guard data.count <= 220 else { 152 | throw RileyLinkDeviceError.writeSizeLimitExceeded(maxLength: 220) 153 | } 154 | 155 | data.insert(UInt8(clamping: data.count), at: 0) 156 | return data 157 | } 158 | } 159 | 160 | 161 | private let log = OSLog(category: "PeripheralManager+RileyLink") 162 | 163 | 164 | extension PeripheralManager { 165 | static let expectedMaxBLELatency: TimeInterval = 2 166 | 167 | var timerTickEnabled: Bool { 168 | return peripheral.getCharacteristicWithUUID(.timerTick)?.isNotifying ?? false 169 | } 170 | 171 | func setTimerTickEnabled(_ enabled: Bool, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) { 172 | perform { (manager) in 173 | do { 174 | guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.timerTick) else { 175 | throw PeripheralManagerError.unknownCharacteristic 176 | } 177 | 178 | try manager.setNotifyValue(enabled, for: characteristic, timeout: timeout) 179 | completion?(nil) 180 | } catch let error as PeripheralManagerError { 181 | completion?(.peripheralManagerError(error)) 182 | } catch { 183 | assertionFailure() 184 | } 185 | } 186 | } 187 | 188 | func setLEDMode(mode: RileyLinkLEDMode) { 189 | perform { (manager) in 190 | do { 191 | guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.ledMode) else { 192 | throw PeripheralManagerError.unknownCharacteristic 193 | } 194 | let value = Data([mode.rawValue]) 195 | try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 196 | } catch (let error) { 197 | assertionFailure(String(describing: error)) 198 | } 199 | } 200 | } 201 | 202 | 203 | 204 | func startIdleListening(idleTimeout: TimeInterval, channel: UInt8, timeout: TimeInterval = expectedMaxBLELatency, completion: @escaping (_ error: RileyLinkDeviceError?) -> Void) { 205 | perform { (manager) in 206 | let command = GetPacket(listenChannel: channel, timeoutMS: UInt32(clamping: Int(idleTimeout.milliseconds))) 207 | 208 | do { 209 | try manager.writeCommandWithoutResponse(command, timeout: timeout) 210 | completion(nil) 211 | } catch let error as RileyLinkDeviceError { 212 | completion(error) 213 | } catch { 214 | assertionFailure() 215 | } 216 | } 217 | } 218 | 219 | func setCustomName(_ name: String, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) { 220 | guard let value = name.data(using: .utf8) else { 221 | completion?(.invalidInput(name)) 222 | return 223 | } 224 | 225 | perform { (manager) in 226 | do { 227 | guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.customName) else { 228 | throw PeripheralManagerError.unknownCharacteristic 229 | } 230 | 231 | try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: timeout) 232 | completion?(nil) 233 | } catch let error as PeripheralManagerError { 234 | completion?(.peripheralManagerError(error)) 235 | } catch { 236 | assertionFailure() 237 | } 238 | } 239 | } 240 | } 241 | 242 | 243 | 244 | // MARK: - Synchronous commands 245 | extension PeripheralManager { 246 | enum ResponseType { 247 | case single 248 | case buffered 249 | } 250 | 251 | /// Invokes a command expecting a response 252 | /// 253 | /// - Parameters: 254 | /// - command: The command 255 | /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error 256 | /// - responseType: The BLE response value framing method 257 | /// - Returns: The received response 258 | /// - Throws: 259 | /// - RileyLinkDeviceError.invalidResponse 260 | /// - RileyLinkDeviceError.peripheralManagerError 261 | /// - RileyLinkDeviceError.writeSizeLimitExceeded 262 | func writeCommand(_ command: C, timeout: TimeInterval, responseType: ResponseType) throws -> C.ResponseType { 263 | guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else { 264 | throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic) 265 | } 266 | 267 | let value = try command.writableData() 268 | 269 | 270 | switch responseType { 271 | case .single: 272 | log.debug("RL Send (single): %@", value.hexadecimalString) 273 | return try writeCommand(value, 274 | for: characteristic, 275 | timeout: timeout 276 | ) 277 | case .buffered: 278 | log.debug("RL Send (buffered): %@", value.hexadecimalString) 279 | return try writeLegacyCommand(value, 280 | for: characteristic, 281 | timeout: timeout, 282 | endOfResponseMarker: 0x00 283 | ) 284 | } 285 | } 286 | 287 | /// Invokes a command without waiting for its response 288 | /// 289 | /// - Parameters: 290 | /// - command: The command 291 | /// - timeout: The amount of time to wait for the peripheral to confirm the write before throwing a timeout error 292 | /// - Throws: 293 | /// - RileyLinkDeviceError.invalidResponse 294 | /// - RileyLinkDeviceError.peripheralManagerError 295 | /// - RileyLinkDeviceError.writeSizeLimitExceeded 296 | fileprivate func writeCommandWithoutResponse(_ command: C, timeout: TimeInterval) throws { 297 | guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else { 298 | throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic) 299 | } 300 | 301 | let value = try command.writableData() 302 | 303 | log.debug("RL Send (no response expected): %@", value.hexadecimalString) 304 | 305 | do { 306 | try writeValue(value, for: characteristic, type: .withResponse, timeout: timeout) 307 | } catch let error as PeripheralManagerError { 308 | throw RileyLinkDeviceError.peripheralManagerError(error) 309 | } 310 | } 311 | 312 | /// - Throws: 313 | /// - RileyLinkDeviceError.invalidResponse 314 | /// - RileyLinkDeviceError.peripheralManagerError 315 | func readRadioFirmwareVersion(timeout: TimeInterval, responseType: ResponseType) throws -> String { 316 | let response = try writeCommand(GetVersion(), timeout: timeout, responseType: responseType) 317 | return response.version 318 | } 319 | 320 | /// - Throws: 321 | /// - RileyLinkDeviceError.invalidResponse 322 | /// - RileyLinkDeviceError.peripheralManagerError 323 | func readBluetoothFirmwareVersion(timeout: TimeInterval) throws -> String { 324 | guard let characteristic = peripheral.getCharacteristicWithUUID(.firmwareVersion) else { 325 | throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic) 326 | } 327 | 328 | do { 329 | guard let data = try readValue(for: characteristic, timeout: timeout) else { 330 | // TODO: This is an "unknown value" issue, not a timeout 331 | throw RileyLinkDeviceError.peripheralManagerError(.timeout) 332 | } 333 | 334 | guard let version = String(bytes: data, encoding: .utf8) else { 335 | throw RileyLinkDeviceError.invalidResponse(data) 336 | } 337 | 338 | return version 339 | } catch let error as PeripheralManagerError { 340 | throw RileyLinkDeviceError.peripheralManagerError(error) 341 | } 342 | } 343 | } 344 | 345 | 346 | // MARK: - Lower-level helper operations 347 | extension PeripheralManager { 348 | 349 | func readBatteryLevel(timeout: TimeInterval) throws -> String { 350 | guard let characteristic = peripheral.getBatteryCharacteristic(.battery_level) else { 351 | throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic) 352 | } 353 | 354 | do { 355 | guard let data = try readValue(for: characteristic, timeout: timeout) else { 356 | // TODO: This is an "unknown value" issue, not a timeout 357 | throw RileyLinkDeviceError.peripheralManagerError(.timeout) 358 | } 359 | 360 | let battery_level = "\(data[0])" 361 | 362 | return battery_level 363 | } catch let error as PeripheralManagerError { 364 | throw RileyLinkDeviceError.peripheralManagerError(error) 365 | } 366 | } 367 | 368 | func setOrangeNotifyOn() throws { 369 | perform { [self] (manager) in 370 | guard let characteristicNotif = peripheral.getOrangeCharacteristic(.orangeNotif) else { 371 | return 372 | } 373 | 374 | add(log: "setOrangeNotifyOn: \(characteristicNotif.uuid.uuidString)") 375 | do { 376 | try setNotifyValue(true, for: characteristicNotif, timeout: 2) 377 | } catch { 378 | add(log: "setOrangeNotifyOn Error: \(error.localizedDescription)") 379 | } 380 | } 381 | } 382 | 383 | func orangeAction(mode: RileyLinkOrangeMode) { 384 | if mode != .off, mode != .shakeOff { 385 | orangeWritePwd() 386 | } 387 | perform { [self] (manager) in 388 | do { 389 | guard let characteristic = peripheral.getOrangeCharacteristic(.orange) else { 390 | throw PeripheralManagerError.unknownCharacteristic 391 | } 392 | let value = Data([0xbb, mode.rawValue]) 393 | add(log: "write: \(value.hexadecimalString)") 394 | try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 395 | } catch (_) { 396 | add(log: "orangeAction failed") 397 | } 398 | } 399 | if mode == .off, mode == .shakeOff { 400 | orangeClose() 401 | } 402 | } 403 | 404 | func findDevices() { 405 | perform { [self] (manager) in 406 | do { 407 | guard let characteristic = peripheral.getOrangeCharacteristic(.orange) else { 408 | throw PeripheralManagerError.unknownCharacteristic 409 | } 410 | let value = Data([0xdd, 0x04]) 411 | add(log: "write: \(value.hexadecimalString)") 412 | try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 413 | } catch (_) { 414 | add(log: "findDevices failed") 415 | } 416 | } 417 | } 418 | 419 | 420 | 421 | func setAction(index: Int, open: Bool) { 422 | perform { [self] (manager) in 423 | do { 424 | guard let characteristic = peripheral.getOrangeCharacteristic(.orange) else { 425 | throw PeripheralManagerError.unknownCharacteristic 426 | } 427 | if index == 0 { 428 | setDatas[2] = 0 429 | setDatas[3] = open ? 1 : 0 430 | } else if index == 1 { 431 | setDatas[2] = 1 432 | setDatas[3] = open ? 1 : 0 433 | } 434 | let value = Data(setDatas) 435 | add(log: "write: \(value.hexadecimalString)") 436 | try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 437 | } catch (_) { 438 | add(log: "setAction failed") 439 | } 440 | } 441 | } 442 | 443 | func orangeWritePwd() { 444 | perform { [self] (manager) in 445 | do { 446 | guard let characteristic = peripheral.getOrangeCharacteristic(.orange) else { 447 | throw PeripheralManagerError.unknownCharacteristic 448 | } 449 | let value = Data([0xAA]) 450 | add(log: "write: \(value.hexadecimalString)") 451 | try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 452 | } catch (_) { 453 | add(log: "orangeWritePwd failed") 454 | } 455 | } 456 | } 457 | 458 | func orangeReadSet() { 459 | perform { [self] (manager) in 460 | do { 461 | guard let characteristic = peripheral.getOrangeCharacteristic(.orange) else { 462 | throw PeripheralManagerError.unknownCharacteristic 463 | } 464 | let value = Data([0xdd, 0x01]) 465 | add(log: "write: \(value.hexadecimalString)") 466 | try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 467 | } catch (_) { 468 | add(log: "orangeReadSet failed") 469 | } 470 | } 471 | } 472 | 473 | func orangeReadVDC() { 474 | perform { [self] (manager) in 475 | do { 476 | guard let characteristic = peripheral.getOrangeCharacteristic(.orange) else { 477 | throw PeripheralManagerError.unknownCharacteristic 478 | } 479 | let value = Data([0xdd, 0x03]) 480 | add(log: "write: \(value.hexadecimalString)") 481 | try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 482 | } catch (_) { 483 | add(log: "orangeReadSet failed") 484 | } 485 | } 486 | } 487 | 488 | func orangeClose() { 489 | perform { [self] (manager) in 490 | do { 491 | guard let characteristic = peripheral.getOrangeCharacteristic(.orange) else { 492 | throw PeripheralManagerError.unknownCharacteristic 493 | } 494 | let value = Data([0xcc]) 495 | add(log: "write: \(value.hexadecimalString)") 496 | try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency) 497 | } catch (_) { 498 | add(log: "orangeClose failed") 499 | } 500 | } 501 | } 502 | 503 | /// Writes command data expecting a single response 504 | /// 505 | /// - Parameters: 506 | /// - data: The command data 507 | /// - characteristic: The peripheral characteristic to write 508 | /// - type: The type of characteristic write 509 | /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error 510 | /// - Returns: The recieved response 511 | /// - Throws: 512 | /// - RileyLinkDeviceError.invalidResponse 513 | /// - RileyLinkDeviceError.peripheralManagerError 514 | private func writeCommand(_ data: Data, 515 | for characteristic: CBCharacteristic, 516 | type: CBCharacteristicWriteType = .withResponse, 517 | timeout: TimeInterval 518 | ) throws -> R 519 | { 520 | var capturedResponse: R? 521 | 522 | do { 523 | try runCommand(timeout: timeout) { 524 | if case .withResponse = type { 525 | addCondition(.write(characteristic: characteristic)) 526 | } 527 | 528 | addCondition(.valueUpdate(characteristic: characteristic, matching: { value in 529 | guard let value = value, value.count > 0 else { 530 | log.debug("Empty response from RileyLink. Continuing to listen for command response.") 531 | return false 532 | } 533 | 534 | log.debug("RL Recv(single): %@", value.hexadecimalString) 535 | 536 | guard let code = ResponseCode(rawValue: value[0]) else { 537 | let unknownCode = value[0..<1].hexadecimalString 538 | log.error("Unknown response code from RileyLink: %{public}@. Continuing to listen for command response.", unknownCode) 539 | return false 540 | } 541 | 542 | switch code { 543 | case .commandInterrupted: 544 | // This is expected in cases where an "Idle" GetPacket command is running 545 | log.debug("Idle command interrupted. Continuing to listen for command response.") 546 | return false 547 | default: 548 | guard let response = R(data: value) else { 549 | log.error("Unable to parse response.") 550 | // We don't recognize the contents. Keep listening. 551 | return false 552 | } 553 | log.debug("RileyLink response: %{public}@", String(describing: response)) 554 | capturedResponse = response 555 | return true 556 | } 557 | })) 558 | 559 | peripheral.writeValue(data, for: characteristic, type: type) 560 | } 561 | } catch let error as PeripheralManagerError { 562 | throw RileyLinkDeviceError.peripheralManagerError(error) 563 | } 564 | 565 | guard let response = capturedResponse else { 566 | throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data()) 567 | } 568 | 569 | return response 570 | } 571 | 572 | /// Writes command data expecting a bufferred response 573 | /// 574 | /// - Parameters: 575 | /// - data: The command data 576 | /// - characteristic: The peripheral characteristic to write 577 | /// - type: The type of characteristic write 578 | /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error 579 | /// - endOfResponseMarker: The marker delimiting the end of a response in the buffer 580 | /// - Returns: The received response. In the event of multiple responses in the buffer, the first parsable response is returned. 581 | /// - Throws: 582 | /// - RileyLinkDeviceError.invalidResponse 583 | /// - RileyLinkDeviceError.peripheralManagerError 584 | private func writeLegacyCommand(_ data: Data, 585 | for characteristic: CBCharacteristic, 586 | type: CBCharacteristicWriteType = .withResponse, 587 | timeout: TimeInterval, 588 | endOfResponseMarker: UInt8 589 | ) throws -> R 590 | { 591 | var capturedResponse: R? 592 | var buffer = ResponseBuffer(endMarker: endOfResponseMarker) 593 | 594 | do { 595 | try runCommand(timeout: timeout) { 596 | if case .withResponse = type { 597 | addCondition(.write(characteristic: characteristic)) 598 | } 599 | 600 | addCondition(.valueUpdate(characteristic: characteristic, matching: { value in 601 | guard let value = value else { 602 | return false 603 | } 604 | 605 | log.debug("RL Recv(buffered): %@", value.hexadecimalString) 606 | buffer.append(value) 607 | 608 | for response in buffer.responses { 609 | switch response.code { 610 | case .rxTimeout, .zeroData, .invalidParam, .unknownCommand: 611 | log.debug("RileyLink response: %{public}@", String(describing: response)) 612 | capturedResponse = response 613 | return true 614 | case .commandInterrupted: 615 | // This is expected in cases where an "Idle" GetPacket command is running 616 | log.debug("RileyLink response: %{public}@", String(describing: response)) 617 | case .success: 618 | capturedResponse = response 619 | return true 620 | } 621 | } 622 | 623 | return false 624 | })) 625 | 626 | peripheral.writeValue(data, for: characteristic, type: type) 627 | } 628 | } catch let error as PeripheralManagerError { 629 | throw RileyLinkDeviceError.peripheralManagerError(error) 630 | } 631 | 632 | guard let response = capturedResponse else { 633 | throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data()) 634 | } 635 | 636 | return response 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /OL-patch-files/rileylink_ios/RileyLinkKitUI/RileyLinkDeviceTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RileyLinkDeviceTableViewController.swift 3 | // Naterade 4 | // 5 | // Created by Nathan Racklyeft on 3/5/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import LoopKitUI 11 | import RileyLinkBLEKit 12 | import RileyLinkKit 13 | import os.log 14 | 15 | let CellIdentifier = "Cell" 16 | 17 | public class RileyLinkSwitch: UISwitch { 18 | 19 | public var index: Int = 0 20 | public var section: Int = 0 21 | } 22 | 23 | public class RileyLinkCell: UITableViewCell { 24 | public let switchView = RileyLinkSwitch() 25 | 26 | public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 27 | super.init(style: style, reuseIdentifier: reuseIdentifier) 28 | contentView.addSubview(switchView) 29 | } 30 | 31 | public required init?(coder aDecoder: NSCoder) { 32 | super.init(coder: aDecoder) 33 | } 34 | 35 | public override func layoutSubviews() { 36 | super.layoutSubviews() 37 | switchView.frame = CGRect(x: frame.width - 51 - 20, y: (frame.height - 31) / 2, width: 51, height: 31) 38 | } 39 | } 40 | 41 | public class RileyLinkDeviceTableViewController: UITableViewController { 42 | 43 | private let log = OSLog(category: "RileyLinkDeviceTableViewController") 44 | 45 | public let device: RileyLinkDevice 46 | 47 | private var bleRSSI: Int? 48 | 49 | private var firmwareVersion: String? { 50 | didSet { 51 | guard isViewLoaded else { 52 | return 53 | } 54 | 55 | cellForRow(.version)?.detailTextLabel?.text = firmwareVersion 56 | } 57 | } 58 | 59 | private var fw_hw: String? { 60 | didSet { 61 | guard isViewLoaded else { 62 | return 63 | } 64 | 65 | cellForRow(.orl)?.detailTextLabel?.text = fw_hw 66 | } 67 | } 68 | 69 | private var uptime: TimeInterval? { 70 | didSet { 71 | guard isViewLoaded else { 72 | return 73 | } 74 | 75 | cellForRow(.uptime)?.setDetailAge(uptime) 76 | } 77 | } 78 | 79 | private var battery: String? { 80 | didSet { 81 | guard isViewLoaded else { 82 | return 83 | } 84 | 85 | cellForRow(.battery)?.setDetailBatteryLevel(battery) 86 | } 87 | } 88 | 89 | private var frequency: Measurement? { 90 | didSet { 91 | guard isViewLoaded else { 92 | return 93 | } 94 | 95 | cellForRow(.frequency)?.setDetailFrequency(frequency, formatter: frequencyFormatter) 96 | } 97 | } 98 | 99 | var rssiFetchTimer: Timer? { 100 | willSet { 101 | rssiFetchTimer?.invalidate() 102 | } 103 | } 104 | 105 | private lazy var frequencyFormatter: MeasurementFormatter = { 106 | let formatter = MeasurementFormatter() 107 | 108 | formatter.numberFormatter = decimalFormatter 109 | 110 | return formatter 111 | }() 112 | 113 | 114 | private var appeared = false 115 | 116 | public init(device: RileyLinkDevice) { 117 | self.device = device 118 | 119 | super.init(style: .grouped) 120 | 121 | updateDeviceStatus() 122 | } 123 | 124 | required public init?(coder aDecoder: NSCoder) { 125 | fatalError("init(coder:) has not been implemented") 126 | } 127 | 128 | public override func viewDidLoad() { 129 | super.viewDidLoad() 130 | 131 | title = device.name 132 | 133 | self.observe() 134 | } 135 | 136 | @objc func updateRSSI() { 137 | device.readRSSI() 138 | } 139 | 140 | func updateDeviceStatus() { 141 | device.getStatus { (status) in 142 | DispatchQueue.main.async { 143 | self.firmwareVersion = status.firmwareDescription 144 | self.fw_hw = status.fw_hw 145 | self.ledOn = status.ledOn 146 | self.vibrationOn = status.vibrationOn 147 | self.voltage = status.voltage 148 | 149 | self.tableView.reloadData() 150 | } 151 | } 152 | } 153 | 154 | func updateUptime() { 155 | device.runSession(withName: "Get stats for uptime") { (session) in 156 | do { 157 | let statistics = try session.getRileyLinkStatistics() 158 | DispatchQueue.main.async { 159 | self.uptime = statistics.uptime 160 | } 161 | } catch let error { 162 | self.log.error("Failed to get stats for uptime: %{public}@", String(describing: error)) 163 | } 164 | } 165 | } 166 | 167 | func updateBatteryLevel() { 168 | device.runSession(withName: "Get battery level") { (session) in 169 | let batteryLevel = self.device.getBatterylevel() 170 | DispatchQueue.main.async { 171 | self.battery = batteryLevel 172 | } 173 | } 174 | } 175 | 176 | func orangeClose() { 177 | device.runSession(withName: "Orange Action Close") { (session) in 178 | self.device.orangeClose() 179 | } 180 | } 181 | 182 | func orangeReadSet() { 183 | device.runSession(withName: "orange Read Set") { (session) in 184 | self.device.orangeReadSet() 185 | } 186 | } 187 | 188 | func orangeReadVDC() { 189 | device.runSession(withName: "orange Read Set") { (session) in 190 | self.device.orangeReadVDC() 191 | } 192 | } 193 | 194 | func writePSW() { 195 | device.runSession(withName: "Orange Action PSW") { (session) in 196 | self.device.orangeWritePwd() 197 | } 198 | } 199 | 200 | func orangeAction(index: Int) { 201 | device.runSession(withName: "Orange Action \(index)") { (session) in 202 | self.device.orangeAction(mode: index) 203 | } 204 | } 205 | 206 | func orangeAction(index: Int, open: Bool) { 207 | device.runSession(withName: "Orange Set Action \(index)") { (session) in 208 | self.device.orangeSetAction(index: index, open: open) 209 | } 210 | } 211 | 212 | func findDevices() { 213 | device.runSession(withName: "Find Devices") { (session) in 214 | self.device.findDevices() 215 | } 216 | } 217 | 218 | func updateFrequency() { 219 | 220 | device.runSession(withName: "Get base frequency") { (session) in 221 | do { 222 | let frequency = try session.readBaseFrequency() 223 | DispatchQueue.main.async { 224 | self.frequency = frequency 225 | } 226 | } catch let error { 227 | self.log.error("Failed to get base frequency: %{public}@", String(describing: error)) 228 | } 229 | } 230 | 231 | } 232 | 233 | // References to registered notification center observers 234 | private var notificationObservers: [Any] = [] 235 | 236 | deinit { 237 | for observer in notificationObservers { 238 | NotificationCenter.default.removeObserver(observer) 239 | } 240 | } 241 | 242 | private func observe() { 243 | let center = NotificationCenter.default 244 | let mainQueue = OperationQueue.main 245 | 246 | notificationObservers = [ 247 | center.addObserver(forName: .DeviceNameDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in 248 | if let cell = self?.cellForRow(.customName) { 249 | cell.detailTextLabel?.text = self?.device.name 250 | } 251 | self?.title = self?.device.name 252 | self?.tableView.reloadData() 253 | }, 254 | center.addObserver(forName: .DeviceConnectionStateDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in 255 | if let cell = self?.cellForRow(.connection) { 256 | cell.detailTextLabel?.text = self?.device.peripheralState.description 257 | } 258 | }, 259 | center.addObserver(forName: .DeviceRSSIDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in 260 | self?.bleRSSI = note.userInfo?[RileyLinkDevice.notificationRSSIKey] as? Int 261 | 262 | if let cell = self?.cellForRow(.rssi), let formatter = self?.integerFormatter { 263 | cell.setDetailRSSI(self?.bleRSSI, formatter: formatter) 264 | } 265 | }, 266 | center.addObserver(forName: .DeviceDidStartIdle, object: device, queue: mainQueue) { [weak self] (note) in 267 | self?.updateDeviceStatus() 268 | }, 269 | center.addObserver(forName: .DeviceFW_HWChange, object: device, queue: mainQueue) { [weak self] (note) in 270 | self?.updateDeviceStatus() 271 | }, 272 | ] 273 | } 274 | 275 | public override func viewWillAppear(_ animated: Bool) { 276 | super.viewWillAppear(animated) 277 | 278 | if appeared { 279 | tableView.reloadData() 280 | } 281 | 282 | rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true) 283 | 284 | appeared = true 285 | 286 | updateRSSI() 287 | 288 | updateFrequency() 289 | 290 | updateUptime() 291 | 292 | updateBatteryLevel() 293 | 294 | writePSW() 295 | 296 | orangeReadSet() 297 | 298 | orangeReadVDC() 299 | 300 | orangeAction(index: 9) 301 | } 302 | 303 | public override func viewDidDisappear(_ animated: Bool) { 304 | super.viewDidDisappear(animated) 305 | if redOn || yellowOn { 306 | orangeAction(index: 3) 307 | } 308 | 309 | if shakeOn { 310 | orangeAction(index: 5) 311 | } 312 | } 313 | 314 | public override func viewWillDisappear(_ animated: Bool) { 315 | super.viewWillDisappear(animated) 316 | rssiFetchTimer = nil 317 | } 318 | 319 | 320 | // MARK: - Formatters 321 | 322 | private lazy var dateFormatter: DateFormatter = { 323 | let dateFormatter = DateFormatter() 324 | 325 | dateFormatter.dateStyle = .none 326 | dateFormatter.timeStyle = .medium 327 | 328 | return dateFormatter 329 | }() 330 | 331 | private lazy var integerFormatter = NumberFormatter() 332 | 333 | private lazy var measurementFormatter: MeasurementFormatter = { 334 | let formatter = MeasurementFormatter() 335 | 336 | formatter.numberFormatter = decimalFormatter 337 | 338 | return formatter 339 | }() 340 | 341 | private lazy var decimalFormatter: NumberFormatter = { 342 | let decimalFormatter = NumberFormatter() 343 | 344 | decimalFormatter.numberStyle = .decimal 345 | decimalFormatter.minimumSignificantDigits = 5 346 | 347 | return decimalFormatter 348 | }() 349 | 350 | // MARK: - Table view data source 351 | 352 | private enum Section: Int, CaseCountable { 353 | case device 354 | case alert 355 | case configureCommand 356 | case commands 357 | } 358 | 359 | private enum AlertRow: Int, CaseCountable { 360 | case battery 361 | case voltage 362 | } 363 | 364 | private enum DeviceRow: Int, CaseCountable { 365 | case customName 366 | case version 367 | case rssi 368 | case connection 369 | case uptime 370 | case frequency 371 | case battery 372 | case orl 373 | case voltage 374 | } 375 | 376 | private enum CommandRow: Int, CaseCountable { 377 | case yellow 378 | case red 379 | case shake 380 | case orangePro 381 | } 382 | 383 | private enum ConfigureCommandRow: Int, CaseCountable { 384 | case led 385 | case vibration 386 | } 387 | 388 | private func cellForRow(_ row: DeviceRow) -> UITableViewCell? { 389 | return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.device.rawValue)) 390 | } 391 | 392 | private func cellForRow(_ row: CommandRow) -> UITableViewCell? { 393 | return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.commands.rawValue)) 394 | } 395 | 396 | public override func numberOfSections(in tableView: UITableView) -> Int { 397 | return Section.count 398 | } 399 | 400 | public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 401 | switch Section(rawValue: section)! { 402 | case .device: 403 | return DeviceRow.count 404 | case .commands: 405 | return CommandRow.count - (device.isOrangePro ? 0 : 1) 406 | case .configureCommand: 407 | return ConfigureCommandRow.count 408 | case .alert: 409 | return AlertRow.count 410 | } 411 | } 412 | 413 | @objc 414 | func switchAction(sender: RileyLinkSwitch) { 415 | switch Section(rawValue: sender.section)! { 416 | case .commands: 417 | switch CommandRow(rawValue: sender.index)! { 418 | case .yellow: 419 | if sender.isOn { 420 | orangeAction(index: 1) 421 | } else { 422 | orangeAction(index: 3) 423 | } 424 | yellowOn = sender.isOn 425 | redOn = false 426 | case .red: 427 | if sender.isOn { 428 | orangeAction(index: 2) 429 | } else { 430 | orangeAction(index: 3) 431 | } 432 | yellowOn = false 433 | redOn = sender.isOn 434 | case .shake: 435 | if sender.isOn { 436 | orangeAction(index: 4) 437 | } else { 438 | orangeAction(index: 5) 439 | } 440 | shakeOn = sender.isOn 441 | default: 442 | break 443 | } 444 | case .configureCommand: 445 | switch ConfigureCommandRow(rawValue: sender.index)! { 446 | case .led: 447 | orangeAction(index: 0, open: sender.isOn) 448 | ledOn = sender.isOn 449 | case .vibration: 450 | orangeAction(index: 1, open: sender.isOn) 451 | vibrationOn = sender.isOn 452 | } 453 | default: 454 | break 455 | } 456 | tableView.reloadData() 457 | } 458 | 459 | var yellowOn = false 460 | var redOn = false 461 | var shakeOn = false 462 | private var ledOn: Bool = false 463 | private var vibrationOn: Bool = false 464 | var voltage = "" 465 | 466 | public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 467 | let cell: RileyLinkCell 468 | 469 | if let reusableCell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier) as? RileyLinkCell { 470 | cell = reusableCell 471 | } else { 472 | cell = RileyLinkCell(style: .value1, reuseIdentifier: CellIdentifier) 473 | cell.switchView.addTarget(self, action: #selector(switchAction(sender:)), for: .valueChanged) 474 | } 475 | 476 | let switchView = cell.switchView 477 | switchView.isHidden = true 478 | switchView.index = indexPath.row 479 | switchView.section = indexPath.section 480 | 481 | cell.accessoryType = .none 482 | cell.detailTextLabel?.text = nil 483 | 484 | switch Section(rawValue: indexPath.section)! { 485 | case .device: 486 | switch DeviceRow(rawValue: indexPath.row)! { 487 | case .customName: 488 | cell.textLabel?.text = LocalizedString("Name", comment: "The title of the cell showing device name") 489 | cell.detailTextLabel?.text = device.name 490 | cell.accessoryType = .disclosureIndicator 491 | case .version: 492 | cell.textLabel?.text = LocalizedString("Firmware", comment: "The title of the cell showing firmware version") 493 | cell.detailTextLabel?.text = firmwareVersion 494 | case .connection: 495 | cell.textLabel?.text = LocalizedString("Connection State", comment: "The title of the cell showing BLE connection state") 496 | cell.detailTextLabel?.text = device.peripheralState.description 497 | case .rssi: 498 | cell.textLabel?.text = LocalizedString("Signal Strength", comment: "The title of the cell showing BLE signal strength (RSSI)") 499 | cell.setDetailRSSI(bleRSSI, formatter: integerFormatter) 500 | case .uptime: 501 | cell.textLabel?.text = LocalizedString("Uptime", comment: "The title of the cell showing uptime") 502 | cell.setDetailAge(uptime) 503 | case .frequency: 504 | cell.textLabel?.text = LocalizedString("Frequency", comment: "The title of the cell showing current rileylink frequency") 505 | cell.setDetailFrequency(frequency, formatter: frequencyFormatter) 506 | case .battery: 507 | cell.textLabel?.text = NSLocalizedString("Battery level", comment: "The title of the cell showing battery level") 508 | cell.setDetailBatteryLevel(battery) 509 | case .orl: 510 | cell.textLabel?.text = NSLocalizedString("ORL", comment: "The title of the cell showing ORL") 511 | cell.detailTextLabel?.text = fw_hw 512 | case .voltage: 513 | cell.textLabel?.text = NSLocalizedString("Voltage", comment: "The title of the cell showing ORL") 514 | cell.detailTextLabel?.text = voltage 515 | } 516 | case .alert: 517 | switch AlertRow(rawValue: indexPath.row)! { 518 | case .battery: 519 | var value = "OFF" 520 | let v = UserDefaults.standard.integer(forKey: "battery_alert_value") 521 | if v != 0 { 522 | value = "\(v)%" 523 | } 524 | 525 | cell.accessoryType = .disclosureIndicator 526 | cell.textLabel?.text = NSLocalizedString("Low Battery Alert", comment: "The title of the cell showing battery level") 527 | cell.detailTextLabel?.text = "\(value)" 528 | case .voltage: 529 | var value = "OFF" 530 | let v = UserDefaults.standard.double(forKey: "voltage_alert_value") 531 | if v != 0 { 532 | value = String(format: "%.1f%", v) 533 | } 534 | 535 | cell.accessoryType = .disclosureIndicator 536 | cell.textLabel?.text = NSLocalizedString("Low Voltage Alert", comment: "The title of the cell showing voltage level") 537 | cell.detailTextLabel?.text = "\(value)" 538 | } 539 | case .commands: 540 | cell.accessoryType = .disclosureIndicator 541 | cell.detailTextLabel?.text = nil 542 | 543 | switch CommandRow(rawValue: indexPath.row)! { 544 | case .yellow: 545 | switchView.isHidden = false 546 | cell.accessoryType = .none 547 | switchView.isOn = yellowOn 548 | cell.textLabel?.text = NSLocalizedString("Lighten Yellow LED", comment: "The title of the cell showing Lighten Yellow LED") 549 | case .red: 550 | switchView.isHidden = false 551 | cell.accessoryType = .none 552 | switchView.isOn = redOn 553 | cell.textLabel?.text = NSLocalizedString("Lighten Red LED", comment: "The title of the cell showing Lighten Red LED") 554 | case .shake: 555 | switchView.isHidden = false 556 | switchView.isOn = shakeOn 557 | cell.accessoryType = .none 558 | cell.textLabel?.text = NSLocalizedString("Test Vibrator", comment: "The title of the cell showing Test Vibrator") 559 | case .orangePro: 560 | cell.textLabel?.text = NSLocalizedString("Find Devices", comment: "The title of the cell showing ORL") 561 | cell.detailTextLabel?.text = nil 562 | } 563 | case .configureCommand: 564 | switch ConfigureCommandRow(rawValue: indexPath.row)! { 565 | case .led: 566 | switchView.isHidden = false 567 | switchView.isOn = ledOn 568 | cell.accessoryType = .none 569 | cell.textLabel?.text = NSLocalizedString("Enable Connection State LED", comment: "The title of the cell showing Stop Vibrator") 570 | case .vibration: 571 | switchView.isHidden = false 572 | switchView.isOn = vibrationOn 573 | cell.accessoryType = .none 574 | cell.textLabel?.text = NSLocalizedString("Enable Connection State Vibrator", comment: "The title of the cell showing Stop Vibrator") 575 | } 576 | } 577 | 578 | return cell 579 | } 580 | 581 | public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 582 | switch Section(rawValue: section)! { 583 | case .device: 584 | return LocalizedString("Device", comment: "The title of the section describing the device") 585 | case .commands: 586 | return LocalizedString("Test Commands", comment: "The title of the section describing commands") 587 | case .configureCommand: 588 | return LocalizedString("Configure Commands", comment: "The title of the section describing commands") 589 | case .alert: 590 | return LocalizedString("Alert", comment: "The title of the section describing commands") 591 | } 592 | } 593 | 594 | // MARK: - UITableViewDelegate 595 | 596 | public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 597 | switch Section(rawValue: indexPath.section)! { 598 | case .device: 599 | switch DeviceRow(rawValue: indexPath.row)! { 600 | case .customName: 601 | return true 602 | default: 603 | return false 604 | } 605 | case .commands: 606 | return device.peripheralState == .connected 607 | case .configureCommand: 608 | return device.peripheralState == .connected 609 | case .alert: 610 | return true 611 | } 612 | } 613 | 614 | public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 615 | switch Section(rawValue: indexPath.section)! { 616 | case .device: 617 | switch DeviceRow(rawValue: indexPath.row)! { 618 | case .customName: 619 | let vc = TextFieldTableViewController() 620 | if let cell = tableView.cellForRow(at: indexPath) { 621 | vc.title = cell.textLabel?.text 622 | vc.value = device.name 623 | vc.delegate = self 624 | vc.keyboardType = .default 625 | } 626 | 627 | show(vc, sender: indexPath) 628 | default: 629 | break 630 | } 631 | case .commands: 632 | switch CommandRow(rawValue: indexPath.row)! { 633 | case .orangePro: 634 | findDevices() 635 | default: 636 | break 637 | } 638 | case .configureCommand: 639 | break 640 | case .alert: 641 | switch AlertRow(rawValue: indexPath.row)! { 642 | case .battery: 643 | let alert = UIAlertController.init(title: "Battery level Alert", message: nil, preferredStyle: .actionSheet) 644 | 645 | let action = UIAlertAction.init(title: "OFF", style: .default) { _ in 646 | UserDefaults.standard.setValue(0, forKey: "battery_alert_value") 647 | self.tableView.reloadData() 648 | } 649 | 650 | let action1 = UIAlertAction.init(title: "20", style: .default) { _ in 651 | UserDefaults.standard.setValue(20, forKey: "battery_alert_value") 652 | self.tableView.reloadData() 653 | } 654 | 655 | let action2 = UIAlertAction.init(title: "30", style: .default) { _ in 656 | UserDefaults.standard.setValue(30, forKey: "battery_alert_value") 657 | self.tableView.reloadData() 658 | } 659 | 660 | let action3 = UIAlertAction.init(title: "40", style: .default) { _ in 661 | UserDefaults.standard.setValue(40, forKey: "battery_alert_value") 662 | self.tableView.reloadData() 663 | } 664 | 665 | let action4 = UIAlertAction.init(title: "50", style: .default) { _ in 666 | UserDefaults.standard.setValue(50, forKey: "battery_alert_value") 667 | self.tableView.reloadData() 668 | } 669 | alert.addAction(action) 670 | alert.addAction(action1) 671 | alert.addAction(action2) 672 | alert.addAction(action3) 673 | alert.addAction(action4) 674 | present(alert, animated: true, completion: nil) 675 | case .voltage: 676 | let alert = UIAlertController.init(title: "Voltage level Alert", message: nil, preferredStyle: .actionSheet) 677 | 678 | let action = UIAlertAction.init(title: "OFF", style: .default) { _ in 679 | UserDefaults.standard.setValue(0, forKey: "voltage_alert_value") 680 | self.tableView.reloadData() 681 | } 682 | 683 | let action1 = UIAlertAction.init(title: "2.4", style: .default) { _ in 684 | UserDefaults.standard.setValue(2.4, forKey: "voltage_alert_value") 685 | self.tableView.reloadData() 686 | } 687 | 688 | let action2 = UIAlertAction.init(title: "2.5", style: .default) { _ in 689 | UserDefaults.standard.setValue(2.5, forKey: "voltage_alert_value") 690 | self.tableView.reloadData() 691 | } 692 | 693 | let action3 = UIAlertAction.init(title: "2.6", style: .default) { _ in 694 | UserDefaults.standard.setValue(2.6, forKey: "voltage_alert_value") 695 | self.tableView.reloadData() 696 | } 697 | 698 | let action4 = UIAlertAction.init(title: "2.7", style: .default) { _ in 699 | UserDefaults.standard.setValue(2.7, forKey: "voltage_alert_value") 700 | self.tableView.reloadData() 701 | } 702 | 703 | let action5 = UIAlertAction.init(title: "2.8", style: .default) { _ in 704 | UserDefaults.standard.setValue(2.8, forKey: "voltage_alert_value") 705 | self.tableView.reloadData() 706 | } 707 | 708 | let action6 = UIAlertAction.init(title: "2.9", style: .default) { _ in 709 | UserDefaults.standard.setValue(2.9, forKey: "voltage_alert_value") 710 | self.tableView.reloadData() 711 | } 712 | 713 | let action7 = UIAlertAction.init(title: "3.0", style: .default) { _ in 714 | UserDefaults.standard.setValue(3.0, forKey: "voltage_alert_value") 715 | self.tableView.reloadData() 716 | } 717 | alert.addAction(action) 718 | alert.addAction(action1) 719 | alert.addAction(action2) 720 | alert.addAction(action3) 721 | alert.addAction(action4) 722 | alert.addAction(action5) 723 | alert.addAction(action6) 724 | alert.addAction(action7) 725 | present(alert, animated: true, completion: nil) 726 | } 727 | } 728 | } 729 | } 730 | 731 | 732 | extension RileyLinkDeviceTableViewController: TextFieldTableViewControllerDelegate { 733 | public func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { 734 | _ = navigationController?.popViewController(animated: true) 735 | } 736 | 737 | public func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) { 738 | if let indexPath = tableView.indexPathForSelectedRow { 739 | switch Section(rawValue: indexPath.section)! { 740 | case .device: 741 | switch DeviceRow(rawValue: indexPath.row)! { 742 | case .customName: 743 | device.setCustomName(controller.value!) 744 | default: 745 | break 746 | } 747 | default: 748 | break 749 | } 750 | } 751 | } 752 | } 753 | 754 | private extension TimeInterval { 755 | func format(using units: NSCalendar.Unit) -> String? { 756 | let formatter = DateComponentsFormatter() 757 | formatter.allowedUnits = units 758 | formatter.unitsStyle = .full 759 | formatter.zeroFormattingBehavior = .dropLeading 760 | formatter.maximumUnitCount = 2 761 | 762 | return formatter.string(from: self) 763 | } 764 | } 765 | 766 | private extension UITableViewCell { 767 | func setDetailDate(_ date: Date?, formatter: DateFormatter) { 768 | if let date = date { 769 | detailTextLabel?.text = formatter.string(from: date) 770 | } else { 771 | detailTextLabel?.text = "-" 772 | } 773 | } 774 | 775 | func setDetailRSSI(_ decibles: Int?, formatter: NumberFormatter) { 776 | detailTextLabel?.text = formatter.decibleString(from: decibles) ?? "-" 777 | } 778 | 779 | func setDetailAge(_ age: TimeInterval?) { 780 | if let age = age { 781 | detailTextLabel?.text = age.format(using: [.day, .hour, .minute]) 782 | } else { 783 | detailTextLabel?.text = "" 784 | } 785 | } 786 | 787 | func setDetailBatteryLevel(_ batteryLevel: String?) { 788 | if let unwrappedBatteryLevel = batteryLevel { 789 | detailTextLabel?.text = unwrappedBatteryLevel + " %" 790 | } else { 791 | detailTextLabel?.text = "" 792 | } 793 | } 794 | 795 | func setDetailFrequency(_ frequency: Measurement?, formatter: MeasurementFormatter) { 796 | if let frequency = frequency { 797 | detailTextLabel?.text = formatter.string(from: frequency) 798 | } else { 799 | detailTextLabel?.text = "" 800 | } 801 | } 802 | 803 | } 804 | -------------------------------------------------------------------------------- /OL-patch-files/rileylink_ios/MinimedKitUI/RileyLinkMinimedDeviceTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RileyLinkMinimedDeviceTableViewController.swift 3 | // Naterade 4 | // 5 | // Created by Nathan Racklyeft on 3/5/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreBluetooth 11 | import LoopKitUI 12 | import MinimedKit 13 | import RileyLinkBLEKit 14 | import RileyLinkKit 15 | import RileyLinkKitUI 16 | 17 | let CellIdentifier = "Cell" 18 | 19 | public class RileyLinkMinimedDeviceTableViewController: UITableViewController { 20 | 21 | public let device: RileyLinkDevice 22 | 23 | private let ops: PumpOps 24 | 25 | private var pumpState: PumpState? { 26 | didSet { 27 | // Update the UI if its visible 28 | guard rssiFetchTimer != nil else { return } 29 | 30 | if let cell = cellForRow(.awake) { 31 | cell.setAwakeUntil(pumpState?.awakeUntil, formatter: dateFormatter) 32 | } 33 | 34 | if let cell = cellForRow(.model) { 35 | cell.setPumpModel(pumpState?.pumpModel) 36 | } 37 | 38 | if let cell = cellForRow(.tune) { 39 | cell.setTuneInfo(lastValidFrequency: pumpState?.lastValidFrequency, lastTuned: pumpState?.lastTuned, measurementFormatter: measurementFormatter, dateFormatter: dateFormatter) 40 | } 41 | } 42 | } 43 | 44 | private var bleRSSI: Int? 45 | 46 | private var firmwareVersion: String? { 47 | didSet { 48 | guard isViewLoaded else { 49 | return 50 | } 51 | 52 | cellForRow(.version)?.detailTextLabel?.text = firmwareVersion 53 | } 54 | } 55 | 56 | private var fw_hw: String? { 57 | didSet { 58 | guard isViewLoaded else { 59 | return 60 | } 61 | 62 | cellForRow(.orl)?.detailTextLabel?.text = fw_hw 63 | } 64 | } 65 | 66 | private var uptime: TimeInterval? { 67 | didSet { 68 | guard isViewLoaded else { 69 | return 70 | } 71 | 72 | cellForRow(.uptime)?.setDetailAge(uptime) 73 | } 74 | } 75 | 76 | private var battery: String? { 77 | didSet { 78 | guard isViewLoaded else { 79 | return 80 | } 81 | 82 | cellForRow(.battery)?.setDetailBatteryLevel(battery) 83 | } 84 | } 85 | 86 | 87 | private var lastIdle: Date? { 88 | didSet { 89 | guard isViewLoaded else { 90 | return 91 | } 92 | 93 | cellForRow(.idleStatus)?.setDetailDate(lastIdle, formatter: dateFormatter) 94 | } 95 | } 96 | 97 | private var rssiFetchTimer: Timer? { 98 | willSet { 99 | rssiFetchTimer?.invalidate() 100 | } 101 | } 102 | 103 | private var appeared = false 104 | 105 | public init(device: RileyLinkDevice, pumpOps: PumpOps) { 106 | self.device = device 107 | self.ops = pumpOps 108 | self.pumpState = pumpOps.pumpState.value 109 | 110 | super.init(style: .grouped) 111 | 112 | updateDeviceStatus() 113 | } 114 | 115 | required public init?(coder aDecoder: NSCoder) { 116 | fatalError("init(coder:) has not been implemented") 117 | } 118 | 119 | public override func viewDidLoad() { 120 | super.viewDidLoad() 121 | 122 | title = device.name 123 | 124 | self.observe() 125 | } 126 | 127 | @objc func updateRSSI() { 128 | device.readRSSI() 129 | } 130 | 131 | func updateUptime() { 132 | device.runSession(withName: "Get stats for uptime") { (session) in 133 | do { 134 | let statistics = try session.getRileyLinkStatistics() 135 | DispatchQueue.main.async { 136 | self.uptime = statistics.uptime 137 | } 138 | } catch { } 139 | } 140 | } 141 | 142 | func updateBatteryLevel() { 143 | device.runSession(withName: "Get battery level") { (session) in 144 | let batteryLevel = self.device.getBatterylevel() 145 | DispatchQueue.main.async { 146 | self.battery = batteryLevel 147 | } 148 | } 149 | } 150 | 151 | 152 | func orangeClose() { 153 | device.runSession(withName: "Orange Action Close") { (session) in 154 | self.device.orangeClose() 155 | } 156 | } 157 | 158 | func orangeReadSet() { 159 | device.runSession(withName: "orange Read Set") { (session) in 160 | self.device.orangeReadSet() 161 | } 162 | } 163 | 164 | func orangeReadVDC() { 165 | device.runSession(withName: "orange Read Set") { (session) in 166 | self.device.orangeReadVDC() 167 | } 168 | } 169 | 170 | func writePSW() { 171 | device.runSession(withName: "Orange Action PSW") { (session) in 172 | self.device.orangeWritePwd() 173 | } 174 | } 175 | 176 | func orangeAction(index: Int) { 177 | device.runSession(withName: "Orange Action \(index)") { (session) in 178 | self.device.orangeAction(mode: index) 179 | } 180 | } 181 | 182 | func orangeAction(index: Int, open: Bool) { 183 | device.runSession(withName: "Orange Set Action \(index)") { (session) in 184 | self.device.orangeSetAction(index: index, open: open) 185 | } 186 | } 187 | 188 | func findDevices() { 189 | device.runSession(withName: "Find Devices") { (session) in 190 | self.device.findDevices() 191 | } 192 | } 193 | 194 | private func updateDeviceStatus() { 195 | device.getStatus { (status) in 196 | DispatchQueue.main.async { 197 | self.firmwareVersion = status.firmwareDescription 198 | self.fw_hw = status.fw_hw 199 | self.ledOn = status.ledOn 200 | self.vibrationOn = status.vibrationOn 201 | self.voltage = status.voltage 202 | 203 | self.tableView.reloadData() 204 | } 205 | } 206 | } 207 | 208 | // References to registered notification center observers 209 | private var notificationObservers: [Any] = [] 210 | 211 | deinit { 212 | for observer in notificationObservers { 213 | NotificationCenter.default.removeObserver(observer) 214 | } 215 | } 216 | 217 | private func observe() { 218 | let center = NotificationCenter.default 219 | let mainQueue = OperationQueue.main 220 | 221 | notificationObservers = [ 222 | center.addObserver(forName: .DeviceNameDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in 223 | if let cell = self?.cellForRow(.customName) { 224 | cell.detailTextLabel?.text = self?.device.name 225 | } 226 | 227 | self?.title = self?.device.name 228 | self?.tableView.reloadData() 229 | }, 230 | center.addObserver(forName: .DeviceConnectionStateDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in 231 | if let cell = self?.cellForRow(.connection) { 232 | cell.detailTextLabel?.text = self?.device.peripheralState.description 233 | } 234 | }, 235 | center.addObserver(forName: .DeviceRSSIDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in 236 | self?.bleRSSI = note.userInfo?[RileyLinkDevice.notificationRSSIKey] as? Int 237 | 238 | if let cell = self?.cellForRow(.rssi), let formatter = self?.integerFormatter { 239 | cell.setDetailRSSI(self?.bleRSSI, formatter: formatter) 240 | } 241 | }, 242 | center.addObserver(forName: .DeviceDidStartIdle, object: device, queue: mainQueue) { [weak self] (note) in 243 | self?.updateDeviceStatus() 244 | }, 245 | center.addObserver(forName: .PumpOpsStateDidChange, object: ops, queue: mainQueue) { [weak self] (note) in 246 | if let state = note.userInfo?[PumpOps.notificationPumpStateKey] as? PumpState { 247 | self?.pumpState = state 248 | } 249 | }, 250 | center.addObserver(forName: .DeviceFW_HWChange, object: device, queue: mainQueue) { [weak self] (note) in 251 | self?.updateDeviceStatus() 252 | }, 253 | ] 254 | } 255 | 256 | public override func viewWillAppear(_ animated: Bool) { 257 | super.viewWillAppear(animated) 258 | 259 | if appeared { 260 | tableView.reloadData() 261 | } 262 | 263 | rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true) 264 | 265 | appeared = true 266 | 267 | updateRSSI() 268 | 269 | updateUptime() 270 | 271 | updateBatteryLevel() 272 | 273 | writePSW() 274 | 275 | orangeReadSet() 276 | 277 | orangeReadVDC() 278 | 279 | orangeAction(index: 9) 280 | } 281 | 282 | public override func viewDidDisappear(_ animated: Bool) { 283 | super.viewDidDisappear(animated) 284 | if redOn || yellowOn { 285 | orangeAction(index: 3) 286 | } 287 | 288 | if shakeOn { 289 | orangeAction(index: 5) 290 | } 291 | } 292 | 293 | public override func viewWillDisappear(_ animated: Bool) { 294 | super.viewWillDisappear(animated) 295 | rssiFetchTimer = nil 296 | } 297 | 298 | 299 | // MARK: - Formatters 300 | 301 | private lazy var dateFormatter: DateFormatter = { 302 | let dateFormatter = DateFormatter() 303 | 304 | dateFormatter.dateStyle = .none 305 | dateFormatter.timeStyle = .medium 306 | 307 | return dateFormatter 308 | }() 309 | 310 | private lazy var integerFormatter = NumberFormatter() 311 | 312 | private lazy var measurementFormatter: MeasurementFormatter = { 313 | let formatter = MeasurementFormatter() 314 | 315 | formatter.numberFormatter = decimalFormatter 316 | 317 | return formatter 318 | }() 319 | 320 | private lazy var decimalFormatter: NumberFormatter = { 321 | let decimalFormatter = NumberFormatter() 322 | 323 | decimalFormatter.numberStyle = .decimal 324 | decimalFormatter.minimumSignificantDigits = 5 325 | 326 | return decimalFormatter 327 | }() 328 | 329 | // MARK: - Table view data source 330 | 331 | private enum Section: Int, CaseCountable { 332 | case device 333 | case pump 334 | case commands 335 | case alert 336 | case configureCommand 337 | case testCommands 338 | } 339 | 340 | private enum DeviceRow: Int, CaseCountable { 341 | case customName 342 | case version 343 | case rssi 344 | case connection 345 | case uptime 346 | case idleStatus 347 | case battery 348 | case orl 349 | case voltage 350 | } 351 | 352 | private enum PumpRow: Int, CaseCountable { 353 | case id 354 | case model 355 | case awake 356 | } 357 | 358 | private enum CommandRow: Int, CaseCountable { 359 | case tune 360 | case changeTime 361 | case mySentryPair 362 | case dumpHistory 363 | case fetchGlucose 364 | case getPumpModel 365 | case pressDownButton 366 | case readPumpStatus 367 | case readBasalSchedule 368 | case enableLED 369 | case discoverCommands 370 | case getStatistics 371 | } 372 | 373 | private enum ConfigureCommandRow: Int, CaseCountable { 374 | case led 375 | case vibration 376 | } 377 | 378 | private enum TestCommandRow: Int, CaseCountable { 379 | case yellow 380 | case red 381 | case shake 382 | case orangePro 383 | } 384 | 385 | private enum AlertRow: Int, CaseCountable { 386 | case battery 387 | case voltage 388 | } 389 | 390 | @objc 391 | func switchAction(sender: RileyLinkSwitch) { 392 | switch Section(rawValue: sender.section)! { 393 | case .testCommands: 394 | switch TestCommandRow(rawValue: sender.index)! { 395 | case .yellow: 396 | if sender.isOn { 397 | orangeAction(index: 1) 398 | } else { 399 | orangeAction(index: 3) 400 | } 401 | yellowOn = sender.isOn 402 | redOn = false 403 | case .red: 404 | if sender.isOn { 405 | orangeAction(index: 2) 406 | } else { 407 | orangeAction(index: 3) 408 | } 409 | yellowOn = false 410 | redOn = sender.isOn 411 | case .shake: 412 | if sender.isOn { 413 | orangeAction(index: 4) 414 | } else { 415 | orangeAction(index: 5) 416 | } 417 | shakeOn = sender.isOn 418 | default: 419 | break 420 | } 421 | case .configureCommand: 422 | switch ConfigureCommandRow(rawValue: sender.index)! { 423 | case .led: 424 | orangeAction(index: 0, open: sender.isOn) 425 | ledOn = sender.isOn 426 | case .vibration: 427 | orangeAction(index: 1, open: sender.isOn) 428 | vibrationOn = sender.isOn 429 | } 430 | default: 431 | break 432 | } 433 | tableView.reloadData() 434 | } 435 | 436 | private func cellForRow(_ row: DeviceRow) -> UITableViewCell? { 437 | return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.device.rawValue)) 438 | } 439 | 440 | private func cellForRow(_ row: PumpRow) -> UITableViewCell? { 441 | return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.pump.rawValue)) 442 | } 443 | 444 | private func cellForRow(_ row: CommandRow) -> UITableViewCell? { 445 | return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.commands.rawValue)) 446 | } 447 | 448 | public override func numberOfSections(in tableView: UITableView) -> Int { 449 | return Section.count 450 | } 451 | 452 | public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 453 | switch Section(rawValue: section)! { 454 | case .device: 455 | return DeviceRow.count 456 | case .pump: 457 | return PumpRow.count 458 | case .commands: 459 | return CommandRow.count 460 | case .configureCommand: 461 | return ConfigureCommandRow.count 462 | case .testCommands: 463 | return TestCommandRow.count - (device.isOrangePro ? 0 : 1) 464 | case .alert: 465 | return AlertRow.count 466 | } 467 | } 468 | 469 | var yellowOn = false 470 | var redOn = false 471 | var shakeOn = false 472 | private var ledOn: Bool = false 473 | private var vibrationOn: Bool = false 474 | var voltage = "" 475 | 476 | public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 477 | let cell: RileyLinkCell 478 | 479 | if let reusableCell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier) as? RileyLinkCell { 480 | cell = reusableCell 481 | } else { 482 | cell = RileyLinkCell(style: .value1, reuseIdentifier: CellIdentifier) 483 | cell.switchView.addTarget(self, action: #selector(switchAction(sender:)), for: .valueChanged) 484 | } 485 | 486 | let switchView = cell.switchView 487 | switchView.isHidden = true 488 | switchView.index = indexPath.row 489 | switchView.section = indexPath.section 490 | 491 | cell.accessoryType = .none 492 | cell.detailTextLabel?.text = nil 493 | 494 | switch Section(rawValue: indexPath.section)! { 495 | case .device: 496 | switch DeviceRow(rawValue: indexPath.row)! { 497 | case .customName: 498 | cell.textLabel?.text = LocalizedString("Name", comment: "The title of the cell showing device name") 499 | cell.detailTextLabel?.text = device.name 500 | cell.accessoryType = .disclosureIndicator 501 | case .version: 502 | cell.textLabel?.text = LocalizedString("Firmware", comment: "The title of the cell showing firmware version") 503 | cell.detailTextLabel?.text = firmwareVersion 504 | case .connection: 505 | cell.textLabel?.text = LocalizedString("Connection State", comment: "The title of the cell showing BLE connection state") 506 | cell.detailTextLabel?.text = device.peripheralState.description 507 | case .rssi: 508 | cell.textLabel?.text = LocalizedString("Signal Strength", comment: "The title of the cell showing BLE signal strength (RSSI)") 509 | cell.setDetailRSSI(bleRSSI, formatter: integerFormatter) 510 | case .uptime: 511 | cell.textLabel?.text = LocalizedString("Uptime", comment: "The title of the cell showing uptime") 512 | cell.setDetailAge(uptime) 513 | case .idleStatus: 514 | cell.textLabel?.text = LocalizedString("On Idle", comment: "The title of the cell showing the last idle") 515 | cell.setDetailDate(lastIdle, formatter: dateFormatter) 516 | case .battery: 517 | cell.textLabel?.text = NSLocalizedString("Battery Level", comment: "The title of the cell showing battery level") 518 | cell.setDetailBatteryLevel(battery) 519 | case .orl: 520 | cell.textLabel?.text = NSLocalizedString("ORL", comment: "The title of the cell showing ORL") 521 | cell.detailTextLabel?.text = fw_hw 522 | case .voltage: 523 | cell.textLabel?.text = NSLocalizedString("Voltage", comment: "The title of the cell showing ORL") 524 | cell.detailTextLabel?.text = voltage 525 | } 526 | case .pump: 527 | switch PumpRow(rawValue: indexPath.row)! { 528 | case .id: 529 | cell.textLabel?.text = LocalizedString("Pump ID", comment: "The title of the cell showing pump ID") 530 | cell.detailTextLabel?.text = ops.pumpSettings.pumpID 531 | case .model: 532 | cell.textLabel?.text = LocalizedString("Pump Model", comment: "The title of the cell showing the pump model number") 533 | cell.setPumpModel(pumpState?.pumpModel) 534 | case .awake: 535 | cell.setAwakeUntil(pumpState?.awakeUntil, formatter: dateFormatter) 536 | } 537 | case .commands: 538 | cell.accessoryType = .disclosureIndicator 539 | cell.detailTextLabel?.text = nil 540 | 541 | switch CommandRow(rawValue: indexPath.row)! { 542 | case .tune: 543 | cell.setTuneInfo(lastValidFrequency: pumpState?.lastValidFrequency, lastTuned: pumpState?.lastTuned, measurementFormatter: measurementFormatter, dateFormatter: dateFormatter) 544 | case .changeTime: 545 | cell.textLabel?.text = LocalizedString("Change Time", comment: "The title of the command to change pump time") 546 | 547 | let localTimeZone = TimeZone.current 548 | let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier 549 | 550 | if let pumpTimeZone = pumpState?.timeZone { 551 | let timeZoneDiff = TimeInterval(pumpTimeZone.secondsFromGMT() - localTimeZone.secondsFromGMT()) 552 | let formatter = DateComponentsFormatter() 553 | formatter.allowedUnits = [.hour, .minute] 554 | let diffString = timeZoneDiff != 0 ? formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff)) : "" 555 | 556 | cell.detailTextLabel?.text = String(format: LocalizedString("%1$@%2$@%3$@", comment: "The format string for displaying an offset from a time zone: (1: GMT)(2: -)(3: 4:00)"), localTimeZoneName, timeZoneDiff != 0 ? (timeZoneDiff < 0 ? "-" : "+") : "", diffString) 557 | } else { 558 | cell.detailTextLabel?.text = localTimeZoneName 559 | } 560 | case .mySentryPair: 561 | cell.textLabel?.text = LocalizedString("MySentry Pair", comment: "The title of the command to pair with mysentry") 562 | 563 | case .dumpHistory: 564 | cell.textLabel?.text = LocalizedString("Fetch Recent History", comment: "The title of the command to fetch recent history") 565 | 566 | case .fetchGlucose: 567 | cell.textLabel?.text = LocalizedString("Fetch Enlite Glucose", comment: "The title of the command to fetch recent glucose") 568 | 569 | case .getPumpModel: 570 | cell.textLabel?.text = LocalizedString("Get Pump Model", comment: "The title of the command to get pump model") 571 | 572 | case .pressDownButton: 573 | cell.textLabel?.text = LocalizedString("Send Button Press", comment: "The title of the command to send a button press") 574 | 575 | case .readPumpStatus: 576 | cell.textLabel?.text = LocalizedString("Read Pump Status", comment: "The title of the command to read pump status") 577 | 578 | case .readBasalSchedule: 579 | cell.textLabel?.text = LocalizedString("Read Basal Schedule", comment: "The title of the command to read basal schedule") 580 | 581 | case .enableLED: 582 | cell.textLabel?.text = LocalizedString("Enable Diagnostic LEDs", comment: "The title of the command to enable diagnostic LEDs") 583 | 584 | case .discoverCommands: 585 | cell.textLabel?.text = LocalizedString("Discover Commands", comment: "The title of the command to discover commands") 586 | 587 | case .getStatistics: 588 | cell.textLabel?.text = LocalizedString("RileyLink Statistics", comment: "The title of the command to fetch RileyLink statistics") 589 | } 590 | 591 | case .alert: 592 | switch AlertRow(rawValue: indexPath.row)! { 593 | case .battery: 594 | var value = "OFF" 595 | let v = UserDefaults.standard.integer(forKey: "battery_alert_value") 596 | if v != 0 { 597 | value = "\(v)%" 598 | } 599 | 600 | cell.accessoryType = .disclosureIndicator 601 | cell.textLabel?.text = NSLocalizedString("Low Battery Alert", comment: "The title of the cell showing battery level") 602 | cell.detailTextLabel?.text = "\(value)" 603 | case .voltage: 604 | var value = "OFF" 605 | let v = UserDefaults.standard.double(forKey: "voltage_alert_value") 606 | if v != 0 { 607 | value = String(format: "%.1f%", v) 608 | } 609 | 610 | cell.accessoryType = .disclosureIndicator 611 | cell.textLabel?.text = NSLocalizedString("Low Voltage Alert", comment: "The title of the cell showing voltage level") 612 | cell.detailTextLabel?.text = "\(value)" 613 | } 614 | case .testCommands: 615 | cell.accessoryType = .disclosureIndicator 616 | cell.detailTextLabel?.text = nil 617 | 618 | switch TestCommandRow(rawValue: indexPath.row)! { 619 | case .yellow: 620 | switchView.isHidden = false 621 | cell.accessoryType = .none 622 | switchView.isOn = yellowOn 623 | cell.textLabel?.text = NSLocalizedString("Lighten Yellow LED", comment: "The title of the cell showing Lighten Yellow LED") 624 | case .red: 625 | switchView.isHidden = false 626 | cell.accessoryType = .none 627 | switchView.isOn = redOn 628 | cell.textLabel?.text = NSLocalizedString("Lighten Red LED", comment: "The title of the cell showing Lighten Red LED") 629 | case .shake: 630 | switchView.isHidden = false 631 | switchView.isOn = shakeOn 632 | cell.accessoryType = .none 633 | cell.textLabel?.text = NSLocalizedString("Test Vibrator", comment: "The title of the cell showing Test Vibrator") 634 | case .orangePro: 635 | cell.textLabel?.text = NSLocalizedString("Find Devices", comment: "The title of the cell showing Find Devices") 636 | cell.detailTextLabel?.text = nil 637 | } 638 | case .configureCommand: 639 | cell.accessoryType = .disclosureIndicator 640 | cell.detailTextLabel?.text = nil 641 | switch ConfigureCommandRow(rawValue: indexPath.row)! { 642 | case .led: 643 | switchView.isHidden = false 644 | switchView.isOn = ledOn 645 | cell.accessoryType = .none 646 | cell.textLabel?.text = NSLocalizedString("Enable Connection State LED", comment: "The title of the cell showing Stop Vibrator") 647 | case .vibration: 648 | switchView.isHidden = false 649 | switchView.isOn = vibrationOn 650 | cell.accessoryType = .none 651 | cell.textLabel?.text = NSLocalizedString("Enable Connection State Vibrator", comment: "The title of the cell showing Stop Vibrator") 652 | } 653 | } 654 | 655 | return cell 656 | } 657 | 658 | public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 659 | switch Section(rawValue: section)! { 660 | case .device: 661 | return LocalizedString("Device", comment: "The title of the section describing the device") 662 | case .pump: 663 | return LocalizedString("Pump", comment: "The title of the section describing the pump") 664 | case .commands: 665 | return LocalizedString("Commands", comment: "The title of the section describing commands") 666 | case .testCommands: 667 | return LocalizedString("Test Commands", comment: "The title of the section describing commands") 668 | case .configureCommand: 669 | return LocalizedString("Configure Commands", comment: "The title of the section describing commands") 670 | case .alert: 671 | return LocalizedString("Alert", comment: "The title of the section describing commands") 672 | } 673 | } 674 | 675 | // MARK: - UITableViewDelegate 676 | 677 | public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 678 | switch Section(rawValue: indexPath.section)! { 679 | case .device: 680 | switch DeviceRow(rawValue: indexPath.row)! { 681 | case .customName: 682 | return true 683 | default: 684 | return false 685 | } 686 | case .pump: 687 | return false 688 | case .commands: 689 | return device.peripheralState == .connected 690 | case .testCommands: 691 | return device.peripheralState == .connected 692 | case .configureCommand: 693 | return device.peripheralState == .connected 694 | case .alert: 695 | return true 696 | } 697 | } 698 | 699 | public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 700 | switch Section(rawValue: indexPath.section)! { 701 | case .device: 702 | switch DeviceRow(rawValue: indexPath.row)! { 703 | case .customName: 704 | let vc = TextFieldTableViewController() 705 | if let cell = tableView.cellForRow(at: indexPath) { 706 | vc.title = cell.textLabel?.text 707 | vc.value = device.name 708 | vc.delegate = self 709 | vc.keyboardType = .default 710 | } 711 | 712 | show(vc, sender: indexPath) 713 | default: 714 | break 715 | } 716 | case .commands: 717 | var vc: CommandResponseViewController? 718 | 719 | switch CommandRow(rawValue: indexPath.row)! { 720 | case .tune: 721 | vc = .tuneRadio(ops: ops, device: device, measurementFormatter: measurementFormatter) 722 | case .changeTime: 723 | vc = .changeTime(ops: ops, device: device) 724 | case .mySentryPair: 725 | vc = .mySentryPair(ops: ops, device: device) 726 | case .dumpHistory: 727 | vc = .dumpHistory(ops: ops, device: device) 728 | case .fetchGlucose: 729 | vc = .fetchGlucose(ops: ops, device: device) 730 | case .getPumpModel: 731 | vc = .getPumpModel(ops: ops, device: device) 732 | case .pressDownButton: 733 | vc = .pressDownButton(ops: ops, device: device) 734 | case .readPumpStatus: 735 | vc = .readPumpStatus(ops: ops, device: device, measurementFormatter: measurementFormatter) 736 | case .readBasalSchedule: 737 | vc = .readBasalSchedule(ops: ops, device: device, integerFormatter: integerFormatter) 738 | case .enableLED: 739 | vc = .enableLEDs(ops: ops, device: device) 740 | case .discoverCommands: 741 | vc = .discoverCommands(ops: ops, device: device) 742 | case .getStatistics: 743 | vc = .getStatistics(ops: ops, device: device) 744 | } 745 | 746 | if let cell = tableView.cellForRow(at: indexPath) { 747 | vc?.title = cell.textLabel?.text 748 | } 749 | 750 | if let vc = vc { 751 | show(vc, sender: indexPath) 752 | } 753 | case .pump: 754 | break 755 | case .testCommands: 756 | switch TestCommandRow(rawValue: indexPath.row)! { 757 | case .orangePro: 758 | findDevices() 759 | default: 760 | break 761 | } 762 | case .configureCommand: 763 | break 764 | case .alert: 765 | switch AlertRow(rawValue: indexPath.row)! { 766 | case .battery: 767 | let alert = UIAlertController.init(title: "Battery level Alert", message: nil, preferredStyle: .actionSheet) 768 | 769 | let action = UIAlertAction.init(title: "OFF", style: .default) { _ in 770 | UserDefaults.standard.setValue(0, forKey: "battery_alert_value") 771 | self.tableView.reloadData() 772 | } 773 | 774 | let action1 = UIAlertAction.init(title: "20", style: .default) { _ in 775 | UserDefaults.standard.setValue(20, forKey: "battery_alert_value") 776 | self.tableView.reloadData() 777 | } 778 | 779 | let action2 = UIAlertAction.init(title: "30", style: .default) { _ in 780 | UserDefaults.standard.setValue(30, forKey: "battery_alert_value") 781 | self.tableView.reloadData() 782 | } 783 | 784 | let action3 = UIAlertAction.init(title: "40", style: .default) { _ in 785 | UserDefaults.standard.setValue(40, forKey: "battery_alert_value") 786 | self.tableView.reloadData() 787 | } 788 | 789 | let action4 = UIAlertAction.init(title: "50", style: .default) { _ in 790 | UserDefaults.standard.setValue(50, forKey: "battery_alert_value") 791 | self.tableView.reloadData() 792 | } 793 | alert.addAction(action) 794 | alert.addAction(action1) 795 | alert.addAction(action2) 796 | alert.addAction(action3) 797 | alert.addAction(action4) 798 | present(alert, animated: true, completion: nil) 799 | case .voltage: 800 | let alert = UIAlertController.init(title: "Voltage level Alert", message: nil, preferredStyle: .actionSheet) 801 | 802 | let action = UIAlertAction.init(title: "OFF", style: .default) { _ in 803 | UserDefaults.standard.setValue(0, forKey: "voltage_alert_value") 804 | self.tableView.reloadData() 805 | } 806 | 807 | let action1 = UIAlertAction.init(title: "2.4", style: .default) { _ in 808 | UserDefaults.standard.setValue(2.4, forKey: "voltage_alert_value") 809 | self.tableView.reloadData() 810 | } 811 | 812 | let action2 = UIAlertAction.init(title: "2.5", style: .default) { _ in 813 | UserDefaults.standard.setValue(2.5, forKey: "voltage_alert_value") 814 | self.tableView.reloadData() 815 | } 816 | 817 | let action3 = UIAlertAction.init(title: "2.6", style: .default) { _ in 818 | UserDefaults.standard.setValue(2.6, forKey: "voltage_alert_value") 819 | self.tableView.reloadData() 820 | } 821 | 822 | let action4 = UIAlertAction.init(title: "2.7", style: .default) { _ in 823 | UserDefaults.standard.setValue(2.7, forKey: "voltage_alert_value") 824 | self.tableView.reloadData() 825 | } 826 | 827 | let action5 = UIAlertAction.init(title: "2.8", style: .default) { _ in 828 | UserDefaults.standard.setValue(2.8, forKey: "voltage_alert_value") 829 | self.tableView.reloadData() 830 | } 831 | 832 | let action6 = UIAlertAction.init(title: "2.9", style: .default) { _ in 833 | UserDefaults.standard.setValue(2.9, forKey: "voltage_alert_value") 834 | self.tableView.reloadData() 835 | } 836 | 837 | let action7 = UIAlertAction.init(title: "3.0", style: .default) { _ in 838 | UserDefaults.standard.setValue(3.0, forKey: "voltage_alert_value") 839 | self.tableView.reloadData() 840 | } 841 | alert.addAction(action) 842 | alert.addAction(action1) 843 | alert.addAction(action2) 844 | alert.addAction(action3) 845 | alert.addAction(action4) 846 | alert.addAction(action5) 847 | alert.addAction(action6) 848 | alert.addAction(action7) 849 | present(alert, animated: true, completion: nil) 850 | } 851 | } 852 | } 853 | } 854 | 855 | 856 | extension RileyLinkMinimedDeviceTableViewController: TextFieldTableViewControllerDelegate { 857 | public func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { 858 | _ = navigationController?.popViewController(animated: true) 859 | } 860 | 861 | public func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) { 862 | if let indexPath = tableView.indexPathForSelectedRow { 863 | switch Section(rawValue: indexPath.section)! { 864 | case .device: 865 | switch DeviceRow(rawValue: indexPath.row)! { 866 | case .customName: 867 | device.setCustomName(controller.value!) 868 | default: 869 | break 870 | } 871 | default: 872 | break 873 | 874 | } 875 | } 876 | } 877 | } 878 | 879 | private extension TimeInterval { 880 | func format(using units: NSCalendar.Unit) -> String? { 881 | let formatter = DateComponentsFormatter() 882 | formatter.allowedUnits = units 883 | formatter.unitsStyle = .full 884 | formatter.zeroFormattingBehavior = .dropLeading 885 | formatter.maximumUnitCount = 2 886 | 887 | return formatter.string(from: self) 888 | } 889 | } 890 | 891 | 892 | private extension UITableViewCell { 893 | 894 | func setDetailBatteryLevel(_ batteryLevel: String?) { 895 | if let unwrappedBatteryLevel = batteryLevel { 896 | detailTextLabel?.text = unwrappedBatteryLevel + " %" 897 | } else { 898 | detailTextLabel?.text = "" 899 | } 900 | } 901 | 902 | 903 | func setDetailDate(_ date: Date?, formatter: DateFormatter) { 904 | if let date = date { 905 | detailTextLabel?.text = formatter.string(from: date) 906 | } else { 907 | detailTextLabel?.text = "-" 908 | } 909 | } 910 | 911 | func setDetailRSSI(_ decibles: Int?, formatter: NumberFormatter) { 912 | detailTextLabel?.text = formatter.decibleString(from: decibles) ?? "-" 913 | } 914 | 915 | func setDetailAge(_ age: TimeInterval?) { 916 | if let age = age { 917 | detailTextLabel?.text = age.format(using: [.day, .hour, .minute]) 918 | } else { 919 | detailTextLabel?.text = "" 920 | } 921 | } 922 | 923 | func setAwakeUntil(_ awakeUntil: Date?, formatter: DateFormatter) { 924 | switch awakeUntil { 925 | case let until? where until.timeIntervalSinceNow < 0: 926 | textLabel?.text = LocalizedString("Last Awake", comment: "The title of the cell describing an awake radio") 927 | setDetailDate(until, formatter: formatter) 928 | case let until?: 929 | textLabel?.text = LocalizedString("Awake Until", comment: "The title of the cell describing an awake radio") 930 | setDetailDate(until, formatter: formatter) 931 | default: 932 | textLabel?.text = LocalizedString("Listening Off", comment: "The title of the cell describing no radio awake data") 933 | detailTextLabel?.text = nil 934 | } 935 | } 936 | 937 | func setPumpModel(_ pumpModel: PumpModel?) { 938 | if let pumpModel = pumpModel { 939 | detailTextLabel?.text = String(describing: pumpModel) 940 | } else { 941 | detailTextLabel?.text = LocalizedString("Unknown", comment: "The detail text for an unknown pump model") 942 | } 943 | } 944 | 945 | func setTuneInfo(lastValidFrequency: Measurement?, lastTuned: Date?, measurementFormatter: MeasurementFormatter, dateFormatter: DateFormatter) { 946 | if let frequency = lastValidFrequency, let date = lastTuned { 947 | textLabel?.text = measurementFormatter.string(from: frequency) 948 | setDetailDate(date, formatter: dateFormatter) 949 | } else { 950 | textLabel?.text = LocalizedString("Tune Radio Frequency", comment: "The title of the command to re-tune the radio") 951 | } 952 | } 953 | 954 | } 955 | --------------------------------------------------------------------------------