├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── documentation_needed.md │ └── feature_request.md └── workflows │ ├── podBuild.yml │ ├── podRelease.yml │ └── swiftBuild.yml ├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── XyBleSdk │ ├── Constants │ └── XYConstants.swift │ ├── Devices │ ├── Agents │ │ ├── XYCentralAgent.swift │ │ └── XYConnectionAgent.swift │ ├── Base │ │ ├── XYBluetoothBase.swift │ │ ├── XYBluetoothDevice.swift │ │ ├── XYBluetoothDeviceBase.swift │ │ ├── XYBluetoothError.swift │ │ ├── XYBluetoothResult.swift │ │ ├── XYFinderDevice.swift │ │ ├── XYFinderDeviceBase.swift │ │ ├── XYFinderDeviceEvent.swift │ │ └── XYFinderDeviceEventManager.swift │ ├── Creators │ │ ├── XY2BluetoothDeviceCreator.swift │ │ ├── XY3BluetoothDeviceCreator.swift │ │ ├── XY4BluetoothDeviceCreator.swift │ │ ├── XYDeviceCreator.swift │ │ ├── XYGPSBluetoothDeviceCreator.swift │ │ └── XYMobileBluetoothDeviceCreator.swift │ ├── XY2BluetoothDevice.swift │ ├── XY3BluetoothDevice.swift │ ├── XY4BluetoothDevice.swift │ ├── XYBluetoothDeviceFactory.swift │ ├── XYDeviceFamily.swift │ ├── XYGPSBluetoothDevice.swift │ ├── XYIBeaconDefinition.swift │ └── XYMobileBluetoothDevice.swift │ ├── Extensions │ └── Array+extensions.swift │ ├── Firmware │ ├── XYFirmwareLoader.swift │ └── XYFirmwareUpdateManager.swift │ ├── Gatt │ ├── GattDescriptors.swift │ ├── GattInquisitor.swift │ ├── GattRequest.swift │ ├── Peripheral │ │ ├── XYCBPeripheralManager .swift │ │ ├── XYMutableCharacteristic.swift │ │ └── XYMutableService.swift │ ├── Standard │ │ ├── AlertNotificationService.swift │ │ ├── BatteryService.swift │ │ ├── CurrentTimeService.swift │ │ ├── DeviceInformationService.swift │ │ ├── GenericAccessService.swift │ │ ├── GenericAttributeService.swift │ │ ├── LinkLossService.swift │ │ ├── OtaService.swift │ │ └── TxPowerService.swift │ ├── XYServiceCharacteristic.swift │ ├── xy3 │ │ ├── BasicConfigService.swift │ │ ├── ControlService.swift │ │ ├── ExtendedConfigService.swift │ │ └── ExtendedControlService.swift │ └── xy4 │ │ └── XYFinderPrimaryService.swift │ ├── Models │ └── XYLocationCoordinate2D.swift │ ├── XYCentral.swift │ ├── XYDeviceCache.swift │ ├── XYDeviceConnectionManager.swift │ ├── XYGeocode.swift │ ├── XYLocation.swift │ ├── XYSmartScan.swift │ └── XyBleSdk.swift ├── Tests ├── LinuxMain.swift └── XyBleSdkTests │ ├── XCTestManifests.swift │ └── XyBleSdkTests.swift └── xcode └── package.xcworkspace └── contents.xcworkspacedata /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the XYO Android SDK 4 | title: '[BUG]' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Observed behavior** 10 | A clear and concise description of what exactly happened. 11 | 12 | **Expected behavior** 13 | A clear and concise description of what you expected to happen. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | 28 | - Device: [e.g. Samsung Galaxy, Google Pixel] 29 | - OS: [e.g. Android 10] 30 | - Browser [e.g. stock browser, chrome] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: XYO Developer Portal 4 | url: https://developers.xyo.network/ 5 | about: XYO Foundation Developer Portal 6 | - name: XYO Foundation Site 7 | url: https://xyo.network/ 8 | about: Check out the fundamentals of our Foundation here -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_needed.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation needed 3 | about: Suggest documentation that is needed 4 | title: '[DOCUMENTATION]:' 5 | labels: documentation 6 | assignees: '' 7 | --- 8 | 9 | **Is your documentation request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the documentation and format you would like** 13 | A clear and concise description of what documentation you would like to see and what type for format. 14 | Ex. Step-by-step, Paragraph explainer, screenshots, etc. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the document request here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for XYO SDK Android 4 | title: '[FEATURE]' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. This could include specific devices, android versions, etc. -------------------------------------------------------------------------------- /.github/workflows/podBuild.yml: -------------------------------------------------------------------------------- 1 | name: Pod Build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "master" 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macOS-latest 12 | strategy: 13 | matrix: 14 | destination: ['platform=iOS Simulator,OS=13.3,name=iPhone 11'] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Install 20 | run: | 21 | pod install 22 | pod update 23 | 24 | - name: Force xcode 11 25 | run: sudo xcode-select -switch /Applications/Xcode_11.3.app 26 | 27 | - name: Clean 28 | env: 29 | destination: ${{ matrix.destination }} 30 | run: xcodebuild clean -workspace XyBleSdk.xcworkspace -scheme "XyBleSdk iOS" -destination "${destination}" 31 | 32 | - name: build 33 | env: 34 | destination: ${{ matrix.destination }} 35 | run: xcodebuild build -workspace XyBleSdk.xcworkspace -scheme "XyBleSdk iOS" -destination "${destination}" -------------------------------------------------------------------------------- /.github/workflows/podRelease.yml: -------------------------------------------------------------------------------- 1 | name: Pod Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macOS-latest 12 | strategy: 13 | matrix: 14 | destination: ['platform=iOS Simulator,OS=13.3,name=iPhone 11'] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Install 20 | run: | 21 | pod install 22 | pod update 23 | 24 | - name: Force xcode 11 25 | run: sudo xcode-select -switch /Applications/Xcode_11.3.app 26 | 27 | - name: Clean 28 | env: 29 | destination: ${{ matrix.destination }} 30 | run: xcodebuild clean -workspace XyBleSdk.xcworkspace -scheme "XyBleSdk iOS" -destination "${destination}" 31 | 32 | - name: build 33 | env: 34 | destination: ${{ matrix.destination }} 35 | run: xcodebuild build -workspace XyBleSdk.xcworkspace -scheme "XyBleSdk iOS" -destination "${destination}" 36 | 37 | - name: Release 38 | run: cd $GITHUB_WORKSPACE && bash $GITHUB_WORKSPACE/scripts/deploy.sh -------------------------------------------------------------------------------- /.github/workflows/swiftBuild.yml: -------------------------------------------------------------------------------- 1 | name: Swift Build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "master" 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Build 16 | run: | 17 | swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.0-simulator" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Promises", 6 | "repositoryURL": "https://github.com/google/promises.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "afa9a1ace74e116848d4f743599ab83e584ff8cb", 10 | "version": "1.2.12" 11 | } 12 | }, 13 | { 14 | "package": "XyBaseSdk", 15 | "repositoryURL": "https://github.com/XYOracleNetwork/sdk-base-swift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "a0c64b5332d014b94779d41c3ef4771c6d8b393d", 19 | "version": "2.0.6" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "XyBleSdk", 8 | platforms: [ 9 | .iOS(.v10), 10 | .macOS(.v10_13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "XyBleSdk", 16 | targets: ["XyBleSdk"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | .package(name: "Promises", url: "https://github.com/google/promises.git", from: "1.2.8"), 22 | .package(name: "XyBaseSdk", url: "https://github.com/XYOracleNetwork/sdk-base-swift.git", from: "2.0.0") 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 26 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 27 | .target( 28 | name: "XyBleSdk", 29 | dependencies: ["Promises", "XyBaseSdk"]), 30 | .testTarget( 31 | name: "XyBleSdkTests", 32 | dependencies: ["XyBleSdk"]), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [logo]: https://cdn.xy.company/img/brand/XYO_full_colored.png 2 | 3 | [![logo]](https://xyo.network) 4 | 5 | ![](https://github.com/XYOracleNetwork/sdk-ble-swift/workflows/Pod%20Build/badge.svg?branch=develop) 6 | ![](https://github.com/XYOracleNetwork/sdk-ble-swift/workflows/Swift%20Build/badge.svg?branch=develop) 7 | 8 | [![BCH compliance](https://bettercodehub.com/edge/badge/XYOracleNetwork/sdk-ble-swift?branch=master)](https://bettercodehub.com/) 9 | [![](https://img.shields.io/cocoapods/v/XyBleSdk.svg?style=flat)](https://cocoapods.org/pods/XyBleSdk) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 10 | 11 | 12 | # Swift Bluetooth SDK 13 | 14 | > The XYO Foundation provides this source code available in our efforts to advance the understanding of the XYO Procotol and its possible uses. We continue to maintain this software in the interest of developer education. Usage of this source code is not intended for production. 15 | 16 | Table of Contents 17 | 18 | - [Title](#sdk-ble-ios) 19 | - [Description](#description) 20 | - [Requirements](#security) 21 | - [Install](#install) 22 | - [SDK Overview](#sdk-overview) 23 | - [Code Examples](#code-examples) 24 | - [Sample Projects](#sample-projects) 25 | - [Maintainers](#maintainers) 26 | - [License](#license) 27 | - [Credits](#credits) 28 | 29 | ## Description 30 | 31 | A Bluetooth library, primarily for use with XY Finder devices but can be implemented to communicate with any Bluetooth device, with monitoring capability if the device emits an iBeacon signal. The library is designed to aleviate the delegate-based interaction with Core Bluetooth classes and presents a straightforward API, allowing the developer to write asyncronous code in a syncronous manner. The libray utlizes the [Google Promises](https://github.com/google/promises) library as a dependency. 32 | 33 | ## Requirements 34 | 35 | - iOS 11.0+ 36 | - MacOS 10.13+ 37 | - Xcode 10.1+ 38 | - Swift 4.2+ 39 | 40 | ## Install 41 | 42 | ### Swift Package Manager 43 | 44 | ```swift 45 | .package(url: "https://github.com/XYOracleNetwork/sdk-ble-swift.git", from: "3.1.6") 46 | ``` 47 | 48 | ### CocoaPods 49 | 50 | > Note that CocoaPods support is only for iOS currently 51 | 52 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 53 | 54 | ```bash 55 | $ gem install cocoapods 56 | ``` 57 | 58 | > CocoaPods 1.6.0.beta.2+ is required. 59 | 60 | To integrate into your Xcode project using CocoaPods, specify it in your `Podfile`: 61 | 62 | ```ruby 63 | source 'https://github.com/CocoaPods/Specs.git' 64 | platform :ios, '11.0' 65 | use_frameworks! 66 | 67 | target '' do 68 | pod 'XyBleSdk', '~> 3.0.7' 69 | end 70 | ``` 71 | 72 | Then, run the following command: 73 | 74 | ```bash 75 | $ pod install 76 | ``` 77 | 78 | ### Carthage 79 | 80 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. 81 | 82 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command: 83 | 84 | ```bash 85 | $ brew update 86 | $ brew install carthage 87 | ``` 88 | 89 | To integrate into your Xcode project using Carthage, specify it in your `Cartfile`: 90 | 91 | ```ogdl 92 | github "XYOracleNetwork/sdk-ble-swift" ~> 3.0.7 93 | ``` 94 | 95 | Run `carthage update --use-submodules` to build the framework and drag the built `XyBleSdk.framework`, `FBLPromises.framework` and `Promises.framework` to the _Linked Frameworks and Libraries_ of your Xcode project. Then switch to the _Build Phases_ tab and add a _New run script phase_. Expand _Run Script_ and add the following to the _Shell_ text field: 96 | 97 | /usr/local/bin/carthage copy-frameworks 98 | 99 | Click the + button under _Input Files_ and add: 100 | 101 | $(SRCROOT)/Carthage/Build//Promises.framework 102 | $(SRCROOT)/Carthage/Build//FBLPromises.framework 103 | $(SRCROOT)/Carthage/Build//XyBleSdk.framework 104 | 105 | Finally, you will need to add a _New Copy Files Phase_, selecting _Frameworks_ for the _Destination_ and adding the three frameworks, ensuring the _Code Sign On Copy_ boxes are checked. 106 | 107 | ## SDK Overview 108 | 109 | Talking to a Bluetooth device using Core Bluetooth is a drag. The developer needs to monitor delegate methods from `CBCentral` and `CBPeripheral`, with no clear path to handling multiple connections. Tutorial code for Core Bluetooth is often a chain of use-case specific method calls from within these delegates, which can lead to frustration when trying to apply the code in a more resusable pattern. Bluetooth devices are often not predictable in their reponse times due to firmware and environmental conditions, which can make them tricky to deal with, especially if the application requires multiple, disparate devices connected to operate properly. 110 | 111 | ## Code Examples 112 | 113 | The XyBleSdk provides a simple interface to communicating with an XY Finder or other Bluetooth device. Let's take a look at an example for an XY Finder device: 114 | 115 | ```swift 116 | let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772") 117 | var batteryLevel: Int? 118 | device.connection { 119 | batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger 120 | if let level = batteryLevel, level > 15 { 121 | self.batteryStatus = "Battery at \(level)" 122 | } else { 123 | self.batteryStatus = "Battery level low" 124 | } 125 | } 126 | ``` 127 | 128 | The `XYBluetoothDeviceFactory` can build a device from a string, peripheral, etc. Using `connection` manages the wrangling of the `CBCentral` and associated `CBPeripheral` delegates, ensuring you have a connection before trying any GATT operation(s) in the block. 129 | 130 | The `get`, `set`, and `notify` methods operate on the specified device and block until the result is returned. This allows the developer to write syncronous code without waiting for a callback or delegate method to be called, or deal with the underlying promises directly. Each operation can also take a timeout if so desired; the default is 30 seconds. 131 | 132 | Once all the operations have completed, you can use `then` if there are post actions you wish to run: 133 | 134 | ```swift 135 | let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772") 136 | var batteryLevel: Int = 0 137 | device.connection { 138 | batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger 139 | if let level = batteryLevel, level > 15 { 140 | self.batteryStatus = "Battery at \(level)" 141 | } else { 142 | self.batteryStatus = "Battery level low" 143 | } 144 | }.then { 145 | self.showBatteryNotification(for: batteryLevel) 146 | } 147 | ``` 148 | 149 | You can check for an error from your operations by using `hasError` in the result. The error is of type `XYFinderBluetoothError`. 150 | 151 | ```swift 152 | let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772") 153 | var batteryLevel: Int = 0 154 | device.connection { 155 | batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger 156 | guard batteryLevel.hasError == false else { return } 157 | if let level = batteryLevel, level > 15 { 158 | self.batteryStatus = "Battery at \(level)" 159 | } else { 160 | self.batteryStatus = "Battery level low" 161 | } 162 | }.then { 163 | self.showBatteryNotification(for: batteryLevel) 164 | } 165 | ``` 166 | 167 | If you wish a specific action to always be run regardless of the result, you can use `always`: 168 | 169 | ```swift 170 | let device = XYBluetoothDeviceFactory.build(from: "xy:ibeacon:a44eacf4-0104-0000-0000-5f784c9977b5.20.28772") 171 | var batteryLevel: Int = 0 172 | device.connection { 173 | batteryLevel = device.get(BatteryService.level, timeout: .seconds(10)).asInteger 174 | guard batteryLevel.hasError == false else { return } 175 | if let level = batteryLevel, level > 15 { 176 | self.batteryStatus = "Battery at \(level)" 177 | } else { 178 | self.batteryStatus = "Battery level low" 179 | } 180 | }.then { 181 | self.showBatteryNotification(for: batteryLevel) 182 | }.always { 183 | self.updateView() 184 | } 185 | ``` 186 | 187 | ### Services 188 | 189 | The library provides three types of communication with a Bluetooth device, `get`, `set`, and `notify`. These operate on the characteristic of a GATT service, which is defined with the `XYServiceCharacteristicType` protocol. Add a new service by creating an enumeration that implements this protocol: 190 | 191 | ```swift 192 | public enum MyService: String, XYServiceCharacteristic { 193 | 194 | public var serviceUuid: CBUUID { return MyService.serviceUuid } 195 | 196 | case level 197 | 198 | public var characteristicUuid: CBUUID { 199 | return BatterySeMyServicervice.uuids[self]! 200 | } 201 | 202 | public var characteristicType: XYServiceCharacteristicType { 203 | return .integer 204 | } 205 | 206 | public var displayName: String { 207 | return "My Level" 208 | } 209 | 210 | private static let serviceUuid = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB") 211 | 212 | private static let uuids: [MyService: CBUUID] = [ 213 | .level: CBUUID(string: "00002a19-0000-1000-8000-00805f9b34fb") 214 | ] 215 | 216 | public static var values: [XYServiceCharacteristic] = [ 217 | level 218 | ] 219 | } 220 | ``` 221 | 222 | ### Operation Results 223 | 224 | The `XYBluetoothResult` class wraps the data received from a `get` call and allows data to be passed to a `get` service call. The `XYBluetoothResult` allows for access to the raw data, as well as the convenience methods `asInteger`, `asString` and `asByteArray`. Any error information is available in `error` as an `XYFinderBluetoothError`. 225 | 226 | ### Device Event Notifications 227 | 228 | When a connected device changes state or an operation is performed (such as pressing the button on an XY4+) a `XYFinderEvent` notification is sent out via the `XYFinderDeviceEventManager`. You can subscribe to these events as shown here: 229 | 230 | ```swift 231 | self.subscriptionUuid = XYFinderDeviceEventManager.subscribe(to: [.buttonPressed]) { event in 232 | switch event { 233 | case .buttonPressed(let device, _): 234 | guard let currentDevice = self.selectedDevice, currentDevice == device else { return } 235 | self.buttonPressed(on: device) 236 | default: 237 | break 238 | } 239 | } 240 | ``` 241 | 242 | The result of the `subscribe` call is a subscription UUID that you can use to unsubscribe from further notifcations: 243 | 244 | ```swift 245 | XYFinderDeviceEventManager.unsubscribe(to: [.buttonPressed], referenceKey: self.subscriptionUuid) 246 | ``` 247 | 248 | ### Smart Scan 249 | 250 | The `XYSmartScan` singleton can be used to range and monitor for XY Finder devices. When using the library in an iOS application, it will range for devices in a particular XY Finder device family when put into foreground mode using `switchToForeground`, and use lower power monitoring when placed into backgound mode with `switchToBackground`. The macOS library will locate devices using the `CBCentralManager.scanForPeripherals` method. 251 | 252 | ## Sample Projects 253 | 254 | The library comes with two sample projects, one for macOS and one for iOS. The macOS sample requires you to run `carthage update` in the project directory. The iOS sample requires either `pod install` or `carthage update` to be run. 255 | 256 | ## Maintainers 257 | 258 | - Arie Trouw 259 | - Carter Harrison 260 | 261 | ## License 262 | 263 | See the [LICENSE](LICENSE) file for license details. 264 | 265 | ## Credits 266 | 267 | Made with 🔥and ❄️ by [XYO](https://www.xyo.network) 268 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Constants/XYConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYConstants.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal struct XYConstants { 12 | static let DEVICE_TUNING_SECONDS_INTERVAL_CONNECTED_RSSI_READ = 3 13 | static let DEVICE_TUNING_LOCATION_CHANGE_THRESHOLD = 10.0 14 | static let DEVICE_TUNING_SECONDS_EXIT_CHECK_INTERVAL = 1.0 15 | static let DEVICE_TUNING_SECONDS_WITHOUT_SIGNAL_FOR_EXITING = 12.0 16 | 17 | static let DEVICE_TUNING_SECONDS_WITHOUT_SIGNAL_FOR_EXIT_GAP_SIZE = 2.0 18 | static let DEVICE_TUNING_SECONDS_WITHOUT_SIGNAL_FOR_EXIT_WINDOW_COUNT = 3 19 | static let DEVICE_TUNING_SECONDS_WITHOUT_SIGNAL_FOR_EXIT_WINDOW_SIZE = 2.5 20 | 21 | static let DEVICE_POWER_LOW: UInt8 = 0x04 22 | static let DEVICE_POWER_HIGH: UInt8 = 0x08 23 | static let DEVICE_LOCK_DEFAULT = Data([0x2f, 0xbe, 0xa2, 0x07, 0x52, 0xfe, 0xbf, 0x31, 0x1d, 0xac, 0x5d, 0xfa, 0x7d, 0x77, 0x76, 0x80]) 24 | static let DEVICE_LOCK_XY4 = Data([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]) 25 | 26 | static let DEVICE_CONNECTABLE_SOURCE_UUID_XY4 = NSUUID(uuidString: "00000000-785F-0000-0000-0401F4AC4EA4") 27 | static let DEVICE_CONNECTABLE_SOURCE_UUID_DEFAULT = NSUUID(uuidString: "a500248c-abc2-4206-9bd7-034f4fc9ed10") 28 | } 29 | 30 | public enum XYDeviceProximity: Int { 31 | case none 32 | case outOfRange 33 | case veryFar 34 | case far 35 | case medium 36 | case near 37 | case veryNear 38 | case touching 39 | 40 | public static func fromSignalStrength(_ strength: Int) -> XYDeviceProximity { 41 | if strength == -999 { return XYDeviceProximity.none } 42 | if strength >= -40 { return XYDeviceProximity.touching } 43 | if strength >= -60 { return XYDeviceProximity.veryNear } 44 | if strength >= -70 { return XYDeviceProximity.near } 45 | if strength >= -80 { return XYDeviceProximity.medium } 46 | if strength >= -90 { return XYDeviceProximity.far } 47 | if strength >= -200 { return XYDeviceProximity.veryFar } 48 | return XYDeviceProximity.outOfRange 49 | } 50 | 51 | public static let defaultProximity: Int = -999 52 | } 53 | 54 | public enum XYButtonType2 : Int { 55 | case none 56 | case single 57 | case double 58 | case long 59 | } 60 | 61 | 62 | 63 | public enum XYFinderSong { 64 | case off 65 | case findIt 66 | 67 | public func values(for device: XYDeviceFamily) -> [UInt8] { 68 | switch self { 69 | case .off: 70 | switch device.id { 71 | case XY4BluetoothDevice.id: 72 | return [0xff, 0x03] 73 | default: 74 | return [0xff] 75 | } 76 | case .findIt: 77 | switch device.id { 78 | case XY4BluetoothDevice.id: 79 | return [0x0b, 0x03] 80 | case XY2BluetoothDevice.id: 81 | return [0x01] 82 | default: 83 | return [0x02] 84 | } 85 | } 86 | 87 | } 88 | } 89 | 90 | // A generic semaphore lock with configurable lock amounts and timeout 91 | internal class GenericLock { 92 | 93 | // Default 5 minute wait time 94 | private static let genericLockTimeout: TimeInterval = 300 95 | 96 | private let 97 | semaphore: DispatchSemaphore, 98 | waitTimeout: TimeInterval 99 | 100 | init(_ value: Int = 1, timeout: TimeInterval = GenericLock.genericLockTimeout) { 101 | self.semaphore = DispatchSemaphore(value: value) 102 | self.waitTimeout = timeout 103 | } 104 | 105 | public func lock() { 106 | if self.semaphore.wait(timeout: .now() + self.waitTimeout) == .timedOut { 107 | self.unlock() 108 | } 109 | } 110 | 111 | public func unlock() { 112 | self.semaphore.signal() 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Agents/XYCentralAgent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYCentralAgent.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 11/1/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | import Promises 11 | 12 | // An agent that allows for adding connecting to the Central in an XYBluetoothDevice connection block 13 | // or any other Promises chain 14 | public final class XYCentralAgent: XYCentralDelegate { 15 | private let 16 | central = XYCentral.instance, 17 | delegateKey: String 18 | 19 | private lazy var promise = Promise.pending() 20 | 21 | public init() { 22 | self.delegateKey = "XYCentralAgent:\(UUID.init().uuidString)" 23 | } 24 | 25 | @discardableResult public func powerOn() -> Promise { 26 | guard self.central.state != .poweredOn else { return Promise(()) } 27 | 28 | self.central.setDelegate(self, key: self.delegateKey) 29 | self.central.enable() 30 | 31 | return promise.always(on: XYCentral.centralQueue) { 32 | self.central.removeDelegate(for: self.delegateKey) 33 | } 34 | } 35 | 36 | public func stateChanged(newState: CBManagerState) { 37 | newState == .poweredOn ? 38 | promise.fulfill(()) : 39 | promise.reject(XYBluetoothError.couldNotPowerOnCentral) 40 | } 41 | 42 | // Unused 43 | public func located(peripheral: XYPeripheral) {} 44 | public func discovered(beacon: XYIBeaconDefinition) {} 45 | public func connected(peripheral: XYPeripheral) {} 46 | public func timeout() {} 47 | public func couldNotConnect(peripheral: XYPeripheral) {} 48 | public func disconnected(periperhal: XYPeripheral) {} 49 | } 50 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Agents/XYConnectionAgent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYConnectionAgent.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 11/1/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | import Promises 11 | 12 | // A helper to allow for adding connecting to a peripheral to a connection() operation closure 13 | internal final class XYConnectionAgent: XYCentralDelegate { 14 | private let 15 | central = XYCentral.instance, 16 | delegateKey: String, 17 | device: XYBluetoothDevice 18 | 19 | private static let callTimeout: DispatchTimeInterval = .seconds(30) 20 | private static let queue = DispatchQueue(label: "com.xyfindables.sdk.XYConnectionAgentTimeoutQueue") 21 | private let connectionQueue: DispatchQueue! 22 | private var timer: DispatchSourceTimer? 23 | 24 | private lazy var promise = Promise.pending() 25 | 26 | // 1. Called to set the device to connect to 27 | init(for device: XYBluetoothDevice) { 28 | self.device = device 29 | self.delegateKey = "XYConnectionAgent:\(device.id.shortId)" 30 | self.connectionQueue = DispatchQueue(label: "com.xyfindables.sdk.XYConnectionAgentConnectionQueueFor\(device.id.shortId)") 31 | } 32 | 33 | // 2. Create a connection, or fulfill the promise if the device already is connected 34 | @discardableResult func connect(_ timeout: DispatchTimeInterval? = nil) -> Promise { 35 | guard self.device.peripheral?.state != .connected && self.device.peripheral?.state != .connecting else { 36 | return Promise(()) 37 | } 38 | 39 | self.central.setDelegate(self, key: self.delegateKey) 40 | 41 | // Timeout on connection to the peripheral 42 | let callTimeout = timeout ?? XYConnectionAgent.callTimeout 43 | 44 | self.timer = DispatchSource.makeTimerSource(queue: XYConnectionAgent.queue) 45 | self.timer?.schedule(deadline: DispatchTime.now() + callTimeout) 46 | self.timer?.setEventHandler(handler: { [weak self] in 47 | guard let strong = self else { return } 48 | strong.timer = nil 49 | strong.promise.reject(XYBluetoothError.timedOut) 50 | }) 51 | self.timer?.resume() 52 | 53 | // If we have no peripheral, we'll need to scan for the device 54 | if device.peripheral == nil { 55 | self.central.scan(stopOnNoDelegates: true) 56 | // Otherwise we can just try to connect 57 | } else { 58 | self.central.connect(to: device) 59 | } 60 | 61 | // Ensure we always stop scanning and remove the delegate so this object can get cleaned up 62 | return promise.delay(0.04).always(on: connectionQueue) { 63 | self.central.removeDelegate(for: self.delegateKey) 64 | self.central.stopScan() 65 | } 66 | } 67 | 68 | // 4: Delegate from central.connect(), meaning we have connected and are ready to set/get characteristics 69 | func connected(peripheral: XYPeripheral) { 70 | connectionQueue.async { 71 | guard peripheral.peripheral == self.device.peripheral else { return } 72 | self.promise.fulfill(()) 73 | } 74 | } 75 | 76 | // 3a. Delegate called from scan(), we found the device and now will connect 77 | func located(peripheral: XYPeripheral) { 78 | connectionQueue.async { 79 | if self.device.attachPeripheral(peripheral) { 80 | self.central.connect(to: self.device) 81 | } 82 | } 83 | } 84 | 85 | func couldNotConnect(peripheral: XYPeripheral) { 86 | promise.reject(XYBluetoothError.notConnected) 87 | } 88 | 89 | // Unused in this single connection case 90 | func timeout() {} 91 | func disconnected(periperhal: XYPeripheral) {} 92 | func stateChanged(newState: CBManagerState) {} 93 | func discovered(beacon: XYIBeaconDefinition) {} 94 | } 95 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYBluetoothBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYBluetoothBase.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | // Basic protocol for all BLE devices 12 | public protocol XYBluetoothBase: class { 13 | var rssi: Int { get set } 14 | var powerLevel: UInt8 { get set } 15 | var name: String { get } 16 | var id: String { get } 17 | 18 | var lastPulseTime: Date? { get set } 19 | var totalPulseCount: Int { get } 20 | var lastMonitoredTime: Date? { get set } 21 | 22 | var proximity: XYDeviceProximity { get } 23 | 24 | var rssiRange: (min: Int, max: Int) { get } 25 | 26 | func update(_ rssi: Int, powerLevel: UInt8) 27 | func resetRssi() 28 | 29 | var supportedServices: [CBUUID] { get set } 30 | 31 | var deviceBleQueue: DispatchQueue { get } 32 | } 33 | 34 | public extension XYBluetoothBase { 35 | var proximity: XYDeviceProximity { 36 | return XYDeviceProximity.fromSignalStrength(self.rssi) 37 | } 38 | } 39 | 40 | public func ==(lhs: XYBluetoothBase, rhs: XYBluetoothBase) -> Bool { 41 | return lhs.id == rhs.id 42 | } 43 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYBluetoothDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYBluetoothDevice.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | import CoreLocation 11 | import Promises 12 | 13 | 14 | 15 | // Use for notifying when a property that the client has subscribed to has changed 16 | public protocol XYBluetoothDeviceNotifyDelegate { 17 | func update(for serviceCharacteristic: XYServiceCharacteristic, value: XYBluetoothResult) 18 | } 19 | 20 | // A generic BLE device 21 | public protocol XYBluetoothDevice: XYBluetoothBase { 22 | var family : XYDeviceFamily {get} 23 | var iBeacon : XYIBeaconDefinition? {get} 24 | var peripheral: CBPeripheral? { get set } 25 | var inRange: Bool { get } 26 | var connected: Bool { get } 27 | var markedForDeletion: Bool? { get } 28 | var isUpdatingFirmware: Bool { get } 29 | 30 | func stayConnected(_ value: Bool) 31 | func updatingFirmware(_ value: Bool) 32 | 33 | func connect() 34 | func disconnect() 35 | 36 | func verifyExit(_ callback:((_ exited: Bool) -> Void)?) 37 | 38 | @discardableResult func connection(_ operations: @escaping () throws -> Void) -> Promise 39 | 40 | func get(_ serivceCharacteristic: XYServiceCharacteristic, timeout: DispatchTimeInterval?) -> XYBluetoothResult 41 | func set(_ serivceCharacteristic: XYServiceCharacteristic, value: XYBluetoothResult, timeout: DispatchTimeInterval?, withResponse: Bool) -> XYBluetoothResult 42 | 43 | func subscribe(to serviceCharacteristic: XYServiceCharacteristic, delegate: (key: String, delegate: XYBluetoothDeviceNotifyDelegate)) -> XYBluetoothResult 44 | func unsubscribe(from serviceCharacteristic: XYServiceCharacteristic, key: String) -> XYBluetoothResult 45 | 46 | func subscribe(_ delegate: CBPeripheralDelegate, key: String) 47 | func unsubscribe(for key: String) 48 | 49 | func attachPeripheral(_ peripheral: XYPeripheral) -> Bool 50 | func detachPeripheral() 51 | func detected (_ rssi: Int) 52 | 53 | var state: CBPeripheralState { get } 54 | } 55 | 56 | // MARK: Methods to get, set, or notify on a characteristic using the Promises-based connection work block method below 57 | public extension XYBluetoothDevice { 58 | 59 | var connected: Bool { 60 | return (self.peripheral?.state ?? .disconnected) == .connected 61 | } 62 | 63 | var state: CBPeripheralState { 64 | return self.peripheral?.state ?? .disconnected 65 | } 66 | 67 | func get(_ serivceCharacteristic: XYServiceCharacteristic, timeout: DispatchTimeInterval? = nil) -> XYBluetoothResult { 68 | do { 69 | return try await(serivceCharacteristic.get(from: self, timeout: timeout)) 70 | } catch { 71 | return XYBluetoothResult(error: error as? XYBluetoothError) 72 | } 73 | } 74 | 75 | func set(_ serivceCharacteristic: XYServiceCharacteristic, value: XYBluetoothResult, timeout: DispatchTimeInterval? = nil, withResponse: Bool = true) -> XYBluetoothResult { 76 | do { 77 | try await(serivceCharacteristic.set(to: self, value: value, timeout: timeout, withResponse: withResponse)) 78 | return XYBluetoothResult(data: nil) 79 | } catch { 80 | return XYBluetoothResult(error: error as? XYBluetoothError) 81 | } 82 | } 83 | 84 | func notify(_ serivceCharacteristic: XYServiceCharacteristic, enabled: Bool, timeout: DispatchTimeInterval? = nil) -> XYBluetoothResult { 85 | do { 86 | try await(serivceCharacteristic.notify(for: self, enabled: enabled, timeout: timeout)) 87 | return XYBluetoothResult(data: nil) 88 | } catch { 89 | return XYBluetoothResult(error: error as? XYBluetoothError) 90 | } 91 | } 92 | 93 | func inquire(_ timeout: DispatchTimeInterval? = nil, callback: @escaping (GattDeviceDescriptor) -> Void) -> XYBluetoothResult { 94 | do { 95 | _ = try await(GattInquisitor(timeout).inquire(for: self).then { callback($0) }) 96 | return XYBluetoothResult(data: nil) 97 | } catch { 98 | return XYBluetoothResult(error: error as? XYBluetoothError) 99 | } 100 | } 101 | 102 | } 103 | 104 | // MARK: Connecting to a device in order to complete a block of operations defined above, as well as disconnect from the peripheral 105 | public extension XYBluetoothDevice { 106 | 107 | @discardableResult func connection(_ operations: @escaping () throws -> Void) -> Promise { 108 | // Check range before running operations block 109 | // guard self.inRange else { 110 | // return Promise(XYBluetoothError.deviceNotInRange) 111 | // } 112 | 113 | // Process the queue, adding the connections agents if needed 114 | return Promise(on: self.deviceBleQueue) { 115 | print("STEP 2: Trying to lock for \(self.id.shortId)...") 116 | 117 | // If we don't have a powered on central, we'll see if we can't get that running 118 | if XYCentral.instance.state != .poweredOn { 119 | print("STEP 2a: Trying to power on Central for \(self.id.shortId)...") 120 | try await(XYCentralAgent().powerOn()) 121 | } 122 | 123 | // If we are no connected, use the agent to handle that before running the operations block 124 | if self.peripheral?.state != .connected { 125 | print("STEP 3: Trying to connect for \(self.id.shortId)... STATE is \(self.peripheral?.state.rawValue ?? -1)") 126 | try await(XYConnectionAgent(for: self).connect()) 127 | } 128 | 129 | print("STEP 4: Trying to run operations for \(self.id.shortId)...") 130 | 131 | // Run the requested Gatt operations 132 | try operations() 133 | 134 | }.then(on: self.deviceBleQueue) { 135 | print("STEP 5: All done for \(self.id.shortId)") 136 | } 137 | 138 | } 139 | 140 | 141 | } 142 | 143 | // MARK: Default implementations of protocol methods and variables 144 | public extension XYBluetoothDevice { 145 | 146 | 147 | #if os(iOS) 148 | func beaconRegion(slot: UInt16) -> CLBeaconRegion { 149 | return beaconRegion(self.family.uuid, slot: slot) 150 | } 151 | 152 | // Builds a beacon region for use in XYLocation based on the current XYIBeaconDefinition 153 | func beaconRegion(_ uuid: UUID, slot: UInt16? = nil) -> CLBeaconRegion { 154 | if iBeacon?.hasMinor ?? false, let major = iBeacon?.major, let minor = iBeacon?.minor { 155 | let computedMinor = slot == nil ? minor : ((minor & 0xfff0) | slot!) 156 | return CLBeaconRegion( 157 | proximityUUID: uuid, 158 | major: major, 159 | minor: computedMinor, 160 | identifier: String(format:"%@:4", id)) 161 | } 162 | 163 | if iBeacon?.hasMajor ?? false, let major = iBeacon?.major { 164 | return CLBeaconRegion( 165 | proximityUUID: uuid, 166 | major: major, 167 | identifier: String(format:"%@:4", id)) 168 | } 169 | 170 | return CLBeaconRegion( 171 | proximityUUID: uuid, 172 | identifier: String(format:"%@:4", id)) 173 | } 174 | #endif 175 | } 176 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYBluetoothDeviceBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYBluetoothDeviceBase.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | import CoreLocation 11 | 12 | 13 | // A concrete base class to base any BLE device off of 14 | open class XYBluetoothDeviceBase: NSObject, XYBluetoothBase, XYBluetoothDevice { 15 | public var 16 | firstPulseTime: Date?, 17 | lastPulseTime: Date?, 18 | lastMonitoredTime: Date? 19 | 20 | public internal(set) var 21 | totalPulseCount = 0, 22 | markedForDeletion: Bool? = false, 23 | queuedForConnection: Bool = false 24 | 25 | fileprivate var deviceLock = GenericLock(3) 26 | 27 | internal var verifyCounter = 0 28 | 29 | var _rssi: Int = 0 30 | 31 | public var 32 | rssi: Int { 33 | set { 34 | _rssi = newValue 35 | } 36 | get { 37 | return _rssi 38 | } 39 | } 40 | public var powerLevel: UInt8 41 | 42 | public let 43 | name: String, 44 | id: String 45 | 46 | public let 47 | deviceBleQueue: DispatchQueue, 48 | family : XYDeviceFamily, 49 | iBeacon : XYIBeaconDefinition? 50 | 51 | public fileprivate(set) var rssiRange: (min: Int, max: Int) = (0, 0) { 52 | didSet { 53 | // We use this hook due to how the FindIt app starts to connect to devices, and we don't want to 54 | // connect when they are not in range yet. Once the device is in range, we check if 55 | // it was queued in stayConnected below and try our connection. This allows for not waiting 56 | // for timeouts on GATT operations when a device can't be found. 57 | if self.queuedForConnection && self.isUpdatingFirmware == false && self.inRange { 58 | self.stayConnected(true) 59 | } 60 | } 61 | } 62 | 63 | public var peripheral: CBPeripheral? 64 | 65 | internal var stayConnected: Bool = false 66 | public fileprivate(set) var isUpdatingFirmware: Bool = false 67 | 68 | public lazy var supportedServices = [CBUUID]() 69 | 70 | fileprivate lazy var delegates = [String: CBPeripheralDelegate?]() 71 | fileprivate lazy var notifyDelegates = [String: (serviceCharacteristic: XYServiceCharacteristic, delegate: XYBluetoothDeviceNotifyDelegate?)]() 72 | 73 | public init(_ id: String, rssi: Int = XYDeviceProximity.none.rawValue, family : XYDeviceFamily, iBeacon : XYIBeaconDefinition?) { 74 | self.id = id 75 | self.name = "" 76 | self.powerLevel = 0 77 | self.deviceBleQueue = DispatchQueue(label: "com.xyfindables.sdk.XYBluetoothBaseQueueFor\(id.shortId)") 78 | self.family = family 79 | self.iBeacon = iBeacon 80 | super.init() 81 | self.rssi = rssi 82 | } 83 | 84 | open func detected(_ rssi: Int) {} 85 | 86 | public func update(_ rssi: Int, powerLevel: UInt8) { 87 | if rssi != XYDeviceProximity.defaultProximity { 88 | self.rssi = rssi 89 | } 90 | 91 | // Inital setting of range 92 | if rssiRange.min == 0 && rssiRange.max == 0 { 93 | rssiRange.min = rssi 94 | rssiRange.max = rssi 95 | } else if rssi != 0 { 96 | // Update range 97 | if rssiRange.max < rssi { rssiRange.max = rssi } 98 | if rssiRange.min > rssi { rssiRange.min = rssi } 99 | } 100 | 101 | self.powerLevel = powerLevel 102 | self.totalPulseCount += 1 103 | 104 | if self.firstPulseTime == nil { 105 | self.firstPulseTime = Date() 106 | } 107 | 108 | self.lastPulseTime = Date() 109 | } 110 | 111 | public func resetRssi() { 112 | self.rssi = XYDeviceProximity.defaultProximity 113 | } 114 | 115 | public func verifyExit(_ callback:((_ exited: Bool) -> Void)?) {} 116 | 117 | public var inRange: Bool { 118 | if self.peripheral?.state == .connected { return true } 119 | 120 | let strength = XYDeviceProximity.fromSignalStrength(self.rssi) 121 | guard 122 | strength != .outOfRange && strength != .none 123 | else { return false } 124 | 125 | return true 126 | } 127 | 128 | public func subscribe(_ delegate: CBPeripheralDelegate, key: String) { 129 | guard self.delegates[key] == nil else { return } 130 | self.delegates[key] = delegate 131 | } 132 | 133 | public func unsubscribe(for key: String) { 134 | self.delegates.removeValue(forKey: key) 135 | } 136 | 137 | public func subscribe(to serviceCharacteristic: XYServiceCharacteristic, delegate: (key: String, delegate: XYBluetoothDeviceNotifyDelegate)) -> XYBluetoothResult { 138 | let result = self.notify(serviceCharacteristic, enabled: true) 139 | 140 | if !result.hasError { 141 | self.notifyDelegates[delegate.key] = (serviceCharacteristic, delegate.delegate) 142 | } 143 | 144 | return result 145 | } 146 | 147 | public func unsubscribe(from serviceCharacteristic: XYServiceCharacteristic, key: String) -> XYBluetoothResult { 148 | self.notifyDelegates.removeValue(forKey: key) 149 | return self.notify(serviceCharacteristic, enabled: false) 150 | } 151 | 152 | open func attachPeripheral(_ peripheral: XYPeripheral) -> Bool { 153 | 154 | return false 155 | } 156 | 157 | 158 | public func detachPeripheral() { 159 | self.peripheral = nil 160 | } 161 | 162 | public func updatingFirmware(_ value: Bool) { 163 | self.isUpdatingFirmware = value 164 | } 165 | 166 | // Connects to the device if requested, and the device is both not trying to connect or already has connected 167 | public func stayConnected(_ value: Bool) { 168 | // Do not try to connect/disconnect when the firmware is updating 169 | guard self.isUpdatingFirmware == false else { return } 170 | 171 | self.stayConnected = value 172 | // Only try a connection when in range, otherwise queue this so when it does come into range 173 | // it will auto connect at that time 174 | if self.inRange { 175 | self.stayConnected ? connect() : disconnect() 176 | self.queuedForConnection = false 177 | } else { 178 | self.queuedForConnection = true 179 | } 180 | } 181 | 182 | public func connect() { 183 | XYDeviceConnectionManager.instance.add(device: self) 184 | } 185 | 186 | public func disconnect() { 187 | self.markedForDeletion = true 188 | XYDeviceConnectionManager.instance.remove(for: self.id, disconnect: true) 189 | } 190 | } 191 | 192 | // MARK: CBPeripheralDelegate, passes these on to delegate subscribers for this peripheral 193 | extension XYBluetoothDeviceBase: CBPeripheralDelegate { 194 | public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 195 | guard peripheral == self.peripheral else { return } 196 | self.delegates.forEach { $1?.peripheral?(peripheral, didDiscoverServices: error) } 197 | } 198 | 199 | public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 200 | guard peripheral == self.peripheral else { return } 201 | self.delegates.forEach { $1?.peripheral?(peripheral, didDiscoverCharacteristicsFor: service, error: error) } 202 | } 203 | 204 | public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 205 | guard peripheral == self.peripheral else { return } 206 | self.notifyDelegates 207 | .filter { $0.value.serviceCharacteristic.characteristicUuid == characteristic.uuid } 208 | .forEach { $0.value.delegate?.update(for: $0.value.serviceCharacteristic, value: XYBluetoothResult(data: characteristic.value))} 209 | 210 | self.delegates.forEach { $1?.peripheral?(peripheral, didUpdateValueFor: characteristic, error: error) } 211 | } 212 | 213 | public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { 214 | guard peripheral == self.peripheral else { return } 215 | self.delegates.forEach { $1?.peripheral?(peripheral, didWriteValueFor: characteristic, error: error) } 216 | } 217 | 218 | public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { 219 | guard peripheral == self.peripheral else { return } 220 | self.delegates.forEach { $1?.peripheral?(peripheral, didUpdateNotificationStateFor: characteristic, error: error) } 221 | } 222 | 223 | // We "recursively" call this method, updating the latest rssi value, and also calling detected if it is an XYFinder device 224 | // This is the driver for the distance meters in the primary application 225 | public func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { 226 | guard peripheral == self.peripheral else { return } 227 | self.update(Int(truncating: RSSI), powerLevel: 0x4) 228 | self.delegates.forEach { $1?.peripheral?(peripheral, didReadRSSI: RSSI, error: error) } 229 | (self as? XYFinderDevice)?.detected(RSSI.intValue) 230 | 231 | // TOOD Not sure this is the right place for this... 232 | DispatchQueue.global().asyncAfter(deadline: .now() + TimeInterval(XYConstants.DEVICE_TUNING_SECONDS_INTERVAL_CONNECTED_RSSI_READ)) { 233 | if (peripheral.state == .connected) { 234 | peripheral.readRSSI() 235 | } 236 | } 237 | } 238 | } 239 | 240 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYBluetoothError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYBluetoothError.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/24/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreBluetooth 11 | 12 | // Common general errors from across the set of XY BLE classes/components 13 | public enum XYBluetoothError: Error { 14 | case notConnected 15 | case mismatchedPeripheral 16 | case serviceNotFound 17 | case characteristicNotFound 18 | case dataNotPresent 19 | case timedOut 20 | case peripheralDisconected(state: CBPeripheralState?) 21 | case cbPeripheralDelegateError(Error) 22 | case actionNotSupported 23 | case couldNotConnect 24 | case centralNotPoweredOn 25 | case couldNotPowerOnCentral 26 | case deviceNotInRange 27 | case couldNotUnlock 28 | case unableToUpdateFirmware 29 | case sameImage 30 | 31 | public var toString: String { 32 | switch self { 33 | case .notConnected: 34 | return "Not Connected" 35 | case .mismatchedPeripheral: 36 | return "Mismatched Peripheral" 37 | case .serviceNotFound: 38 | return "Service Not Found" 39 | case .characteristicNotFound: 40 | return "Characteristic Not Found" 41 | case .dataNotPresent: 42 | return "Data Not Present" 43 | case .timedOut: 44 | return "Timed Out" 45 | case .peripheralDisconected(let state): 46 | return "Peripheral Disconnected:\n\(state.debugDescription)" 47 | case .cbPeripheralDelegateError(let error): 48 | return "Peripheral Delegate Error:\n\(error.localizedDescription)" 49 | case .actionNotSupported: 50 | return "Requested Action Not Supported" 51 | case .couldNotConnect: 52 | return "Could Not Connect" 53 | case .centralNotPoweredOn: 54 | return "Bluetooth is Off" 55 | case .couldNotUnlock: 56 | return "Could not unlock" 57 | case .couldNotPowerOnCentral: 58 | return "Could not power on Central" 59 | case .deviceNotInRange: 60 | return "Device not in range" 61 | case .unableToUpdateFirmware: 62 | return "Unable to update firmware" 63 | case .sameImage: 64 | return "Same Image Error" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYBluetoothResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYBluetoothResult.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/13/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Represents the result from any get/set request made via a GattRequest 12 | // This result will contain the raw Data from a get, is used to set raw Data for a set, 13 | // and will contain any error that occurred as part of the operation 14 | public class XYBluetoothResult { 15 | public private(set) var data: Data? 16 | public private(set) var error: XYBluetoothError? 17 | 18 | public init(data: Data?) { 19 | self.data = data 20 | } 21 | 22 | public convenience init(_ data: Data?, error: XYBluetoothError?) { 23 | self.init(data: data) 24 | self.error = error 25 | } 26 | 27 | public convenience init(error: XYBluetoothError?) { 28 | self.init(data: nil) 29 | self.error = error 30 | } 31 | 32 | public static var empty: XYBluetoothResult { return XYBluetoothResult(data: nil) } 33 | 34 | public func setData(_ data: Data?) { self.data = data } 35 | public func setError(_ error: XYBluetoothError?) { self.error = error } 36 | 37 | public var hasError: Bool { return error != nil } 38 | } 39 | 40 | // Display a hex number from Data 41 | extension Collection where Iterator.Element == UInt8 { 42 | public var hexa: String { 43 | return map{ String(format: "%02X", $0) }.joined() 44 | } 45 | } 46 | 47 | public extension XYBluetoothResult { 48 | 49 | var asString: String? { 50 | guard let data = self.data else { return nil } 51 | return String(data: data, encoding: .utf8) 52 | } 53 | 54 | var asInteger: Int? { 55 | guard let data = self.data, data.count > 0 else { return nil } 56 | let array = [UInt8](data) 57 | if (array.count == 1) { 58 | return data.withUnsafeBytes { 59 | let value = $0.load(as: UInt8.self) 60 | return Int(value) 61 | } 62 | } 63 | else if (array.count == 2) { 64 | return data.withUnsafeBytes { 65 | let value = $0.load(as: UInt16.self) 66 | return Int(bigEndian: Int(value)) 67 | } 68 | } 69 | else if (array.count == 4) { 70 | return data.withUnsafeBytes { 71 | let value = $0.load(as: UInt32.self) 72 | return Int(bigEndian: Int(value)) 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | var asByteArray: [UInt8]? { 79 | guard let data = self.data else { return nil } 80 | return Array(data) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYFinderDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYFinderDevice.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreLocation 11 | import CoreBluetooth 12 | 13 | // A device from the XY family, has an iBeacon and other XY-specific identifiers 14 | public protocol XYFinderDevice: XYBluetoothDevice { 15 | var connectableServices: [CBUUID] { get } 16 | var location: XYLocationCoordinate2D { get } 17 | var batteryLevel: Int { get } 18 | var firmware: String { get } 19 | var isRegistered: Bool { get } 20 | 21 | // Handlers for button press subscriptions 22 | @discardableResult func subscribeToButtonPress() -> XYBluetoothResult 23 | @discardableResult func unsubscribeToButtonPress(for referenceKey: UUID?) -> XYBluetoothResult 24 | 25 | // Handle location updates 26 | func updateLocation(_ newLocation: XYLocationCoordinate2D) 27 | 28 | // Updates to battery level 29 | func updateBatteryLevel(_ newLevel: Int) 30 | 31 | // Updates the state of the device's isRegistered flag 32 | // I'm unsure as to what this is used for 33 | func getRegistrationFlag() 34 | 35 | // TODO make this an internal protocol or something... 36 | func startMonitorTimer() 37 | func cancelMonitorTimer() 38 | 39 | // Convenience methods for common operations 40 | @discardableResult 41 | func find(_ song: XYFinderSong) -> XYBluetoothResult 42 | 43 | @discardableResult 44 | func stayAwake() -> XYBluetoothResult 45 | 46 | @discardableResult 47 | func isAwake() -> XYBluetoothResult 48 | 49 | @discardableResult 50 | func fallAsleep() -> XYBluetoothResult 51 | 52 | @discardableResult 53 | func lock() -> XYBluetoothResult 54 | 55 | @discardableResult 56 | func unlock() -> XYBluetoothResult 57 | 58 | @discardableResult 59 | func version() -> XYBluetoothResult 60 | 61 | } 62 | 63 | // MARK: Default handler to report the button press should the finder subscribe to the notification 64 | extension XYFinderDeviceBase: XYBluetoothDeviceNotifyDelegate { 65 | public func update(for serviceCharacteristic: XYServiceCharacteristic, value: XYBluetoothResult) { 66 | guard 67 | // Validate the proper services and the value from the notification, then report 68 | serviceCharacteristic.characteristicUuid == XYFinderPrimaryService.buttonState.characteristicUuid || 69 | serviceCharacteristic.characteristicUuid == ControlService.button.characteristicUuid, 70 | let rawValue = value.asByteArray 71 | else { return } 72 | 73 | let data = Data(_: rawValue) 74 | if let value = (data.withUnsafeBytes { $0.load(as: Int?.self) }) { 75 | let buttonPressed = XYButtonType2.init(rawValue: value)! 76 | 77 | if !self.handlingButtonPress { 78 | self.handlingButtonPress = true 79 | DispatchQueue.main.async { [weak self] in 80 | guard let self = self else {return} 81 | XYFinderDeviceEventManager.report(events: [.buttonPressed(device: self, type: buttonPressed)]) 82 | } 83 | 84 | 85 | XYSmartScan.queue.asyncAfter(deadline: DispatchTime.now() + TimeInterval(3.0)) { 86 | self.handlingButtonPress = false 87 | } 88 | } 89 | } 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYFinderDeviceBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYFinderDeviceBase.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/18/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreLocation 11 | import CoreBluetooth 12 | 13 | // The base XYFinder class 14 | public class XYFinderDeviceBase: XYBluetoothDeviceBase, XYFinderDevice { 15 | public var 16 | location: XYLocationCoordinate2D = XYLocationCoordinate2D(), 17 | isRegistered: Bool = false, 18 | batteryLevel: Int = -1, 19 | firmware: String = "" 20 | 21 | internal var handlingButtonPress: Bool = false 22 | var shouldCheckForButtonPressOnDetection = false 23 | 24 | public init(_ family: XYDeviceFamily, id: String, iBeacon: XYIBeaconDefinition?, rssi: Int) { 25 | super.init(id, rssi: rssi, family: family, iBeacon: iBeacon) 26 | } 27 | 28 | fileprivate static let buttonTimeout: DispatchTimeInterval = .seconds(30) 29 | fileprivate static let buttonTimerQueue = DispatchQueue(label:"com.xyfindables.sdk.XYFinderDeviceButtonTimerQueue") 30 | fileprivate var buttonTimer: DispatchSourceTimer? 31 | 32 | fileprivate static let monitorTimeout: DispatchTimeInterval = .seconds(30) 33 | fileprivate static let monitorTimerQueue = DispatchQueue(label:"com.xyfindables.sdk.XYFinderDeviceMonitorTimerQueue") 34 | fileprivate var monitorTimer: DispatchSourceTimer? 35 | 36 | override public func attachPeripheral(_ peripheral: XYPeripheral) -> Bool { 37 | guard 38 | self.peripheral == nil, 39 | let services = peripheral.advertisementData?[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] 40 | else { return false } 41 | 42 | let connectableServices = self.connectableServices 43 | 44 | guard 45 | connectableServices.count == 2, 46 | services.contains(connectableServices[0]) || services.contains(connectableServices[1]) 47 | else { return false } 48 | 49 | // Set the peripheral and delegate to self 50 | self.peripheral = peripheral.peripheral 51 | self.peripheral?.delegate = self 52 | 53 | // Save off the services this device was found with for BG monitoring 54 | self.supportedServices = services 55 | 56 | return true 57 | } 58 | 59 | 60 | public var connectableServices: [CBUUID] { 61 | guard let major = iBeacon?.major else { 62 | return [] 63 | } 64 | 65 | guard let minor = iBeacon?.minor else { 66 | return [] 67 | } 68 | 69 | func getServiceUuid() -> CBUUID { 70 | let uuidSource = NSUUID(uuidString: "a500248c-abc2-4206-9bd7-034f4fc9ed10") 71 | let uuidBytes = UnsafeMutablePointer.allocate(capacity: 16) 72 | uuidSource?.getBytes(uuidBytes) 73 | for i in (0...11) { 74 | uuidBytes[i] = uuidBytes[i + 4]; 75 | } 76 | uuidBytes[13] = UInt8(major & 0x00ff) 77 | uuidBytes[12] = UInt8((major & 0xff00) >> 8) 78 | uuidBytes[15] = UInt8(minor & 0x00f0) | 0x04 79 | uuidBytes[14] = UInt8((minor & 0xff00) >> 8) 80 | 81 | return CBUUID(data: Data(bytes:uuidBytes, count:16)) 82 | } 83 | 84 | return [XYConstants.DEVICE_POWER_LOW, XYConstants.DEVICE_POWER_HIGH].map { _ in getServiceUuid() } 85 | } 86 | 87 | public func getRegistrationFlag() { 88 | if !self.unlock().hasError { 89 | let result = self.isAwake() 90 | if !result.hasError, let value = result.asByteArray, value.count > 0 { 91 | self.isRegistered = value[0] != 0x00 92 | } 93 | } 94 | } 95 | 96 | 97 | // If while we are monitoring the device we detect it has exited, we start a timer as a device may have just 98 | // triggered the exit while still being close by. Once the timer expires before it enters, we fire the notification 99 | public func startMonitorTimer() { 100 | if monitorTimer == nil { 101 | monitorTimer = DispatchSource.makeTimerSource(queue: XYFinderDeviceBase.monitorTimerQueue) 102 | monitorTimer?.schedule(deadline: DispatchTime.now() + XYFinderDeviceBase.monitorTimeout) 103 | monitorTimer?.setEventHandler(handler: { [weak self] in 104 | guard let strong = self else { return } 105 | strong.monitorTimer = nil 106 | if strong.iBeacon?.hasMajor ?? false && strong.iBeacon?.hasMinor ?? false { 107 | print("MONITOR TIMER EXPIRE: Device \(strong.id)") 108 | strong.verifyExit() 109 | } 110 | }) 111 | monitorTimer?.resume() 112 | } 113 | } 114 | 115 | // If the device enters while monitoring, we always cancel the timer and report it is back 116 | public func cancelMonitorTimer() { 117 | self.monitorTimer = nil 118 | XYFinderDeviceEventManager.report(events: [.entered(device: self)]) 119 | } 120 | 121 | public override func detected(_ rssi: Int) { 122 | guard self.isUpdatingFirmware == false else { return } 123 | 124 | var events: [XYFinderEventNotification] = [.detected(device: self, powerLevel: Int(self.powerLevel), rssi: self.rssi, distance: 0)] 125 | 126 | // If the button has been pressed on a compatible devices, we add the appropriate event 127 | if powerLevel == 8, shouldCheckForButtonPressOnDetection { 128 | if buttonTimer == nil { 129 | buttonTimer = DispatchSource.makeTimerSource(queue: XYFinderDeviceBase.buttonTimerQueue) 130 | buttonTimer?.schedule(deadline: DispatchTime.now() + XYFinderDeviceBase.buttonTimeout) 131 | buttonTimer?.setEventHandler(handler: { [weak self] in 132 | guard let strong = self else { return } 133 | strong.buttonTimer = nil 134 | }) 135 | buttonTimer?.resume() 136 | events.append(.buttonPressed(device: self, type: .single)) 137 | } 138 | } 139 | 140 | if self.isRegistered { 141 | XYFinderDeviceEventManager.report(events: [.updated(device: self)]) 142 | } 143 | 144 | if stayConnected && connected == false { 145 | self.connect() 146 | } 147 | 148 | XYFinderDeviceEventManager.report(events: events) 149 | } 150 | 151 | public override func verifyExit(_ callback:((_ exited: Bool) -> Void)? = nil) { 152 | // If we're connected poke the device 153 | guard self.peripheral?.state != .connected else { 154 | peripheral?.readRSSI() 155 | return 156 | } 157 | 158 | // We set the proximity to none to ensure the visual meters pick up the change, 159 | // and change last pulse time to nil so this method is not picked up again by 160 | // checkExits() 161 | self.rssi = XYDeviceProximity.defaultProximity 162 | self.lastPulseTime = nil 163 | 164 | XYFinderDeviceEventManager.report(events: [.exited(device: self)]) 165 | 166 | // We put the device in the wait queue so it auto-reconnects when it comes back 167 | // into range. This works only while the app is in the background 168 | if XYSmartScan.instance.mode == .background { 169 | XYDeviceConnectionManager.instance.wait(for: self) 170 | } 171 | 172 | // The call back is used for app logic uses only 173 | callback?(true) 174 | } 175 | 176 | // Handles the xy1 and xy2 cases 177 | @discardableResult 178 | public func subscribeToButtonPress() -> XYBluetoothResult { 179 | return XYBluetoothResult.empty 180 | } 181 | 182 | @discardableResult 183 | public func unsubscribeToButtonPress(for referenceKey: UUID? = nil) -> XYBluetoothResult { 184 | return XYBluetoothResult.empty 185 | } 186 | 187 | public func updateLocation(_ newLocation: XYLocationCoordinate2D) { 188 | self.location = newLocation 189 | } 190 | 191 | public func updateBatteryLevel(_ newLevel: Int) { 192 | self.batteryLevel = newLevel 193 | } 194 | 195 | @discardableResult 196 | public func find(_ song: XYFinderSong = .findIt) -> XYBluetoothResult { 197 | return XYBluetoothResult(error: XYBluetoothError.actionNotSupported) 198 | } 199 | 200 | @discardableResult 201 | public func stayAwake() -> XYBluetoothResult { 202 | return XYBluetoothResult(error: XYBluetoothError.actionNotSupported) 203 | } 204 | 205 | @discardableResult 206 | public func isAwake() -> XYBluetoothResult { 207 | return XYBluetoothResult(error: XYBluetoothError.actionNotSupported) 208 | } 209 | 210 | @discardableResult 211 | public func fallAsleep() -> XYBluetoothResult { 212 | return XYBluetoothResult(error: XYBluetoothError.actionNotSupported) 213 | } 214 | 215 | @discardableResult 216 | public func lock() -> XYBluetoothResult { 217 | return XYBluetoothResult(error: XYBluetoothError.actionNotSupported) 218 | } 219 | 220 | @discardableResult 221 | public func unlock() -> XYBluetoothResult { 222 | return XYBluetoothResult(error: XYBluetoothError.actionNotSupported) 223 | } 224 | 225 | @discardableResult 226 | public func version() -> XYBluetoothResult { 227 | return XYBluetoothResult(error: XYBluetoothError.actionNotSupported) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYFinderDeviceEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYFinderDeviceEvent.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/11/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum XYFinderEvent: Int { 12 | case 13 | connected = 0, 14 | alreadyConnected, 15 | connectionError, 16 | reconnected, 17 | disconnected, 18 | buttonPressed, 19 | detected, 20 | entered, 21 | exiting, 22 | exited, 23 | updated, 24 | timedOut 25 | } 26 | 27 | public enum XYFinderTimeoutEvent: Int { 28 | case 29 | connection, 30 | getOperation, 31 | setOperation, 32 | notifyOperation 33 | } 34 | 35 | public enum XYFinderEventNotification { 36 | case connected(device: XYBluetoothDevice) 37 | case alreadyConnected(device: XYBluetoothDevice) 38 | case connectionError(device: XYBluetoothDevice, error: XYBluetoothError?) 39 | case reconnected(device: XYBluetoothDevice) 40 | case disconnected(device: XYBluetoothDevice) 41 | case buttonPressed(device: XYBluetoothDevice, type: XYButtonType2) 42 | case detected(device: XYBluetoothDevice, powerLevel: Int, rssi: Int, distance: Double) 43 | case entered(device: XYBluetoothDevice) 44 | case exiting(device: XYBluetoothDevice) 45 | case exited(device: XYBluetoothDevice) 46 | case updated(device: XYBluetoothDevice) 47 | case timedOut(device: XYBluetoothDevice, type: XYFinderTimeoutEvent) 48 | 49 | // Silly but allows for readble conditionals based on the event's reporting device, as well 50 | // as simplified switch case statements 51 | public var device: XYBluetoothDevice { 52 | switch self { 53 | case .connected(let device): return device 54 | case .alreadyConnected(let device): return device 55 | case .connectionError(let device, _): return device 56 | case .reconnected(let device): return device 57 | case .disconnected(let device): return device 58 | case .buttonPressed(let device, _): return device 59 | case .detected(let device, _, _ , _): return device 60 | case .entered(let device): return device 61 | case .exiting(let device): return device 62 | case .exited(let device): return device 63 | case .updated(let device): return device 64 | case .timedOut(let device, _): return device 65 | } 66 | } 67 | 68 | // Used by the manager to lookup events from dictionary 69 | internal var toEvent: XYFinderEvent { 70 | switch self { 71 | case .connected: return .connected 72 | case .alreadyConnected: return .alreadyConnected 73 | case .connectionError: return .connectionError 74 | case .reconnected: return .reconnected 75 | case .disconnected: return .disconnected 76 | case .buttonPressed: return .buttonPressed 77 | case .detected: return .detected 78 | case .entered: return .entered 79 | case .exiting: return .exiting 80 | case .exited: return .exited 81 | case .updated: return .updated 82 | case .timedOut: return .timedOut 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Base/XYFinderDeviceEventManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYFinderDeviceEventManager.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/11/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias XYFinderDeviceEventNotifier = (_ event: XYFinderEventNotification) -> Void 12 | 13 | internal struct XYFinderDeviceEventDirective { 14 | let 15 | referenceKey: UUID = UUID.init(), 16 | handler: XYFinderDeviceEventNotifier, 17 | device: XYBluetoothDevice? 18 | } 19 | 20 | public class XYFinderDeviceEventManager { 21 | 22 | fileprivate static var handlerRegistry = [XYFinderEvent: [XYFinderDeviceEventDirective]]() 23 | 24 | fileprivate static let managerQueue = DispatchQueue(label: "com.xyfindables.sdk.XYFinderDeviceEventManagerQueue") 25 | 26 | // Notify those directives that want all events and those that subscribe to the event's device 27 | public static func report(events: [XYFinderEventNotification]) { 28 | events.forEach { event in 29 | handlerRegistry[event.toEvent]? 30 | .filter { $0.device == nil || $0.device?.id == event.device.id } 31 | .forEach { $0.handler(event) } 32 | } 33 | } 34 | 35 | // Equivalent to subscribing to every device's events 36 | public static func subscribe(to events: [XYFinderEvent], handler: @escaping XYFinderDeviceEventNotifier) -> UUID { 37 | return subscribe(to: events, for: nil, handler: handler) 38 | } 39 | 40 | // Subscribe to a single device's events. This will simply filter when it comes to reporting to the handlers 41 | public static func subscribe(to events: [XYFinderEvent], for device: XYBluetoothDevice?, handler: @escaping XYFinderDeviceEventNotifier) -> UUID { 42 | let directive = XYFinderDeviceEventDirective(handler: handler, device: device) 43 | managerQueue.async { 44 | events.forEach { event in 45 | handlerRegistry[event] == nil ? 46 | handlerRegistry[event] = [directive] : 47 | handlerRegistry[event]?.append(directive) 48 | } 49 | } 50 | 51 | return directive.referenceKey 52 | } 53 | 54 | public static func unsubscribe(to events: [XYFinderEvent], referenceKey: UUID?) { 55 | managerQueue.async { 56 | guard let key = referenceKey else { return } 57 | for event in events { 58 | guard let eventsInRegistry = handlerRegistry[event] else { continue } 59 | let updatedArray = eventsInRegistry.filter { $0.referenceKey != key } 60 | self.handlerRegistry[event] = updatedArray 61 | } 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Creators/XY2BluetoothDeviceCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XY2BluetoothDeviceCreator.swift 3 | // Pods-SampleiOS 4 | // 5 | // Created by Carter Harrison on 2/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | public struct XY2BluetoothDeviceCreator : XYDeviceCreator { 12 | private init () {} 13 | 14 | public static let uuid : String = XY2BluetoothDevice.uuid 15 | public var family: XYDeviceFamily = XY2BluetoothDevice.family 16 | 17 | public func createFromIBeacon (iBeacon: XYIBeaconDefinition, rssi: Int) -> XYBluetoothDevice? { 18 | return XY2BluetoothDevice(iBeacon: iBeacon, rssi: rssi) 19 | } 20 | 21 | public func createFromId(id: String) -> XYBluetoothDevice { 22 | return XY2BluetoothDevice(id) 23 | } 24 | 25 | public static func enable (enable : Bool) { 26 | if (enable) { 27 | XYBluetoothDeviceFactory.addCreator(uuid: XY2BluetoothDeviceCreator.uuid, creator: XY2BluetoothDeviceCreator()) 28 | } else { 29 | XYBluetoothDeviceFactory.removeCreator(uuid: XY2BluetoothDeviceCreator.uuid) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Creators/XY3BluetoothDeviceCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XY3BluetoothDeviceCreator.swift 3 | // Pods-SampleiOS 4 | // 5 | // Created by Carter Harrison on 2/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct XY3BluetoothDeviceCreator : XYDeviceCreator { 11 | private init () {} 12 | 13 | public static let uuid : String = XY3BluetoothDevice.uuid 14 | public var family: XYDeviceFamily = XY3BluetoothDevice.family 15 | 16 | public func createFromIBeacon (iBeacon: XYIBeaconDefinition, rssi: Int) -> XYBluetoothDevice? { 17 | return XY3BluetoothDevice(iBeacon: iBeacon, rssi: rssi) 18 | } 19 | 20 | public func createFromId(id: String) -> XYBluetoothDevice { 21 | return XY3BluetoothDevice(id) 22 | } 23 | 24 | public static func enable (enable : Bool) { 25 | if (enable) { 26 | XYBluetoothDeviceFactory.addCreator(uuid: XY3BluetoothDeviceCreator.uuid, creator: XY3BluetoothDeviceCreator()) 27 | } else { 28 | XYBluetoothDeviceFactory.removeCreator(uuid: XY3BluetoothDeviceCreator.uuid) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Creators/XY4BluetoothDeviceCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XY4BluetoothDeviceCreator.swift 3 | // Pods-SampleiOS 4 | // 5 | // Created by Carter Harrison on 2/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct XY4BluetoothDeviceCreator : XYDeviceCreator { 11 | 12 | private init () {} 13 | 14 | public static let uuid : String = XY4BluetoothDevice.uuid 15 | public var family: XYDeviceFamily = XY4BluetoothDevice.family 16 | 17 | 18 | public func createFromIBeacon (iBeacon: XYIBeaconDefinition, rssi: Int) -> XYBluetoothDevice? { 19 | return XY4BluetoothDevice(iBeacon: iBeacon, rssi: rssi) 20 | } 21 | 22 | public func createFromId(id: String) -> XYBluetoothDevice { 23 | return XY4BluetoothDevice(id) 24 | } 25 | 26 | public static func enable (enable : Bool) { 27 | if (enable) { 28 | XYBluetoothDeviceFactory.addCreator(uuid: XY4BluetoothDeviceCreator.uuid, creator: XY4BluetoothDeviceCreator()) 29 | } else { 30 | XYBluetoothDeviceFactory.removeCreator(uuid: XY4BluetoothDeviceCreator.uuid) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Creators/XYDeviceCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYDeviceCreator.swift 3 | // Pods-SampleiOS 4 | // 5 | // Created by Carter Harrison on 2/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol XYDeviceCreator { 11 | func createFromIBeacon (iBeacon: XYIBeaconDefinition, rssi: Int) -> XYBluetoothDevice? 12 | func createFromId (id: String) -> XYBluetoothDevice 13 | var family : XYDeviceFamily { get } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Creators/XYGPSBluetoothDeviceCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYGPSBluetoothDeviceCreator.swift 3 | // Pods-SampleiOS 4 | // 5 | // Created by Carter Harrison on 2/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct XYGPSBluetoothDeviceCreator : XYDeviceCreator { 11 | private init () {} 12 | 13 | public static let uuid : String = XYGPSBluetoothDevice.uuid 14 | public var family: XYDeviceFamily = XYGPSBluetoothDevice.family 15 | 16 | 17 | public func createFromIBeacon (iBeacon: XYIBeaconDefinition, rssi: Int) -> XYBluetoothDevice? { 18 | return XYGPSBluetoothDevice(iBeacon: iBeacon, rssi: rssi) 19 | } 20 | 21 | public func createFromId(id: String) -> XYBluetoothDevice { 22 | return XYGPSBluetoothDevice(id) 23 | } 24 | 25 | public static func enable (enable : Bool) { 26 | if (enable) { 27 | XYBluetoothDeviceFactory.addCreator(uuid: XYGPSBluetoothDeviceCreator.uuid, creator: XYGPSBluetoothDeviceCreator()) 28 | } else { 29 | XYBluetoothDeviceFactory.removeCreator(uuid: XYGPSBluetoothDeviceCreator.uuid) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/Creators/XYMobileBluetoothDeviceCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYMobileBluetoothDeviceCreator.swift 3 | // Pods-SampleiOS 4 | // 5 | // Created by Carter Harrison on 2/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct XYMobileBluetoothDeviceCreator : XYDeviceCreator { 11 | private init () {} 12 | 13 | public static let uuid : String = XYMobileBluetoothDevice.uuid 14 | public var family: XYDeviceFamily = XYMobileBluetoothDevice.family 15 | 16 | public func createFromIBeacon (iBeacon: XYIBeaconDefinition, rssi: Int) -> XYBluetoothDevice? { 17 | return XYMobileBluetoothDevice(iBeacon: iBeacon, rssi: rssi) 18 | } 19 | 20 | public func createFromId(id: String) -> XYBluetoothDevice { 21 | return XYMobileBluetoothDevice(id) 22 | } 23 | 24 | public static func enable (enable : Bool) { 25 | if (enable) { 26 | XYBluetoothDeviceFactory.addCreator(uuid: XYMobileBluetoothDeviceCreator.uuid, creator: XYMobileBluetoothDeviceCreator()) 27 | } else { 28 | XYBluetoothDeviceFactory.removeCreator(uuid: XYMobileBluetoothDeviceCreator.uuid) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XY2BluetoothDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XY2BluetoothDevice.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/2/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | // The XY2-specific implementation 12 | public class XY2BluetoothDevice: XYFinderDeviceBase { 13 | public static let family = XYDeviceFamily.init(uuid: UUID(uuidString: XY2BluetoothDevice.uuid)!, 14 | prefix: XY2BluetoothDevice.prefix, 15 | familyName: XY2BluetoothDevice.familyName, 16 | id: XY2BluetoothDevice.id) 17 | 18 | public static let id = "XY2" 19 | public static let uuid : String = "07775dd0-111b-11e4-9191-0800200c9a66" 20 | public static let familyName : String = "XY2 Finder" 21 | public static let prefix : String = "xy:ibeacon" 22 | 23 | public init(_ id: String, iBeacon: XYIBeaconDefinition? = nil, rssi: Int = XYDeviceProximity.none.rawValue) { 24 | super.init(XY2BluetoothDevice.family, id: id, iBeacon: iBeacon, rssi: rssi) 25 | } 26 | 27 | public convenience init(iBeacon: XYIBeaconDefinition, rssi: Int = XYDeviceProximity.none.rawValue) { 28 | self.init(iBeacon.xyId(from: XY2BluetoothDevice.family), iBeacon: iBeacon, rssi: rssi) 29 | } 30 | 31 | @discardableResult 32 | public override func isAwake() -> XYBluetoothResult { 33 | return XYBluetoothResult(data: Data([0x01])) 34 | } 35 | 36 | @discardableResult 37 | public override func find(_ song: XYFinderSong = .findIt) -> XYBluetoothResult { 38 | let songData = Data(song.values(for: self.family)) 39 | return self.set(ControlService.buzzerSelect, value: XYBluetoothResult(data: songData)) 40 | } 41 | 42 | @discardableResult 43 | public override func unlock() -> XYBluetoothResult { 44 | return XYBluetoothResult.init(data: nil) 45 | } 46 | 47 | @discardableResult 48 | public override func version() -> XYBluetoothResult { 49 | self.firmware = "2.0" 50 | return XYBluetoothResult(data: self.firmware.data(using: .utf8)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XY3BluetoothDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XY3BluetoothDevice.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | // The XY3-specific implementation 12 | public class XY3BluetoothDevice: XYFinderDeviceBase { 13 | public static let family = XYDeviceFamily.init(uuid: UUID(uuidString: XY3BluetoothDevice.uuid)!, 14 | prefix: XY3BluetoothDevice.prefix, 15 | familyName: XY3BluetoothDevice.familyName, 16 | id: XY3BluetoothDevice.id) 17 | 18 | public static let id = "XY3" 19 | public static let uuid : String = "08885dd0-111b-11e4-9191-0800200c9a66" 20 | public static let familyName : String = "XY3 Finder" 21 | public static let prefix : String = "xy:ibeacon" 22 | 23 | public init(_ id: String, iBeacon: XYIBeaconDefinition? = nil, rssi: Int = XYDeviceProximity.none.rawValue) { 24 | super.init(XY3BluetoothDevice.family, id: id, iBeacon: iBeacon, rssi: rssi) 25 | super.shouldCheckForButtonPressOnDetection = true 26 | } 27 | 28 | public convenience init(iBeacon: XYIBeaconDefinition, rssi: Int = XYDeviceProximity.none.rawValue) { 29 | self.init(iBeacon.xyId(from: XY3BluetoothDevice.family), iBeacon: iBeacon, rssi: rssi) 30 | } 31 | 32 | public override func subscribeToButtonPress() -> XYBluetoothResult { 33 | return self.subscribe(to: ControlService.button, delegate: (self.id, self)) 34 | } 35 | 36 | public override func unsubscribeToButtonPress(for referenceKey: UUID? = nil) -> XYBluetoothResult { 37 | return self.unsubscribe(from: ControlService.button, key: referenceKey?.uuidString ?? self.id) 38 | } 39 | 40 | @discardableResult 41 | override public func find(_ song: XYFinderSong = .findIt) -> XYBluetoothResult { 42 | let songData = Data(song.values(for: self.family)) 43 | return self.set(ControlService.buzzerSelect, value: XYBluetoothResult(data: songData)) 44 | } 45 | 46 | @discardableResult 47 | public override func stayAwake() -> XYBluetoothResult { 48 | return self.set(ExtendedConfigService.registration, value: XYBluetoothResult(data: Data([0x01]))) 49 | } 50 | 51 | @discardableResult 52 | public override func isAwake() -> XYBluetoothResult { 53 | return self.get(ExtendedConfigService.registration) 54 | } 55 | 56 | @discardableResult 57 | public override func fallAsleep() -> XYBluetoothResult { 58 | return self.set(ExtendedConfigService.registration, value: XYBluetoothResult(data: Data([0x00]))) 59 | } 60 | 61 | @discardableResult 62 | public override func lock() -> XYBluetoothResult { 63 | return self.set(BasicConfigService.lock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_DEFAULT)) 64 | } 65 | 66 | @discardableResult 67 | public override func unlock() -> XYBluetoothResult { 68 | return self.set(BasicConfigService.unlock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_DEFAULT)) 69 | } 70 | 71 | @discardableResult 72 | public override func version() -> XYBluetoothResult { 73 | let result = self.get(ControlService.version) 74 | let version = result.data?.map { String($0, radix: 16) }.joined() 75 | return XYBluetoothResult(data: version?.data(using: .utf8)) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XY4BluetoothDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XY4BluetoothDevice.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | // The XY4-specific implementation 12 | public class XY4BluetoothDevice: XYFinderDeviceBase { 13 | public static let family = XYDeviceFamily.init(uuid: UUID(uuidString: XY4BluetoothDevice.uuid)!, 14 | prefix: XY4BluetoothDevice.prefix, 15 | familyName: XY4BluetoothDevice.familyName, 16 | id: XY4BluetoothDevice.id) 17 | 18 | public static let id = "XY4" 19 | public static let uuid : String = "a44eacf4-0104-0000-0000-5f784c9977b5" 20 | public static let familyName : String = "XY4 Finder" 21 | public static let prefix : String = "xy:ibeacon" 22 | 23 | public init(_ id: String, iBeacon: XYIBeaconDefinition? = nil, rssi: Int = XYDeviceProximity.none.rawValue) { 24 | super.init(XY4BluetoothDevice.family, id: id, iBeacon: iBeacon, rssi: rssi) 25 | super.shouldCheckForButtonPressOnDetection = true 26 | } 27 | 28 | public convenience init(iBeacon: XYIBeaconDefinition, rssi: Int = XYDeviceProximity.none.rawValue) { 29 | self.init(iBeacon.xyId(from: XY4BluetoothDevice.family), iBeacon: iBeacon, rssi: rssi) 30 | } 31 | 32 | public override var connectableServices: [CBUUID] { 33 | guard let major = iBeacon?.major else { 34 | return [] 35 | } 36 | 37 | guard let minor = iBeacon?.minor else { 38 | return [] 39 | } 40 | 41 | func getServiceUuid(_ connectablePowerLevel: UInt8) -> CBUUID { 42 | let uuidSource = NSUUID(uuidString: "00000000-785F-0000-0000-0401F4AC4EA4") 43 | let uuidBytes = UnsafeMutablePointer.allocate(capacity: 16) 44 | uuidSource?.getBytes(uuidBytes) 45 | 46 | uuidBytes[2] = UInt8(major & 0x00ff) 47 | uuidBytes[3] = UInt8((major & 0xff00) >> 8) 48 | uuidBytes[0] = UInt8(minor & 0x00f0) | connectablePowerLevel 49 | uuidBytes[1] = UInt8((minor & 0xff00) >> 8) 50 | 51 | return CBUUID(data: Data(bytes:uuidBytes, count:16)) 52 | } 53 | 54 | return [XYConstants.DEVICE_POWER_LOW, XYConstants.DEVICE_POWER_HIGH].map { 55 | getServiceUuid($0) 56 | } 57 | } 58 | 59 | @discardableResult 60 | public override func subscribeToButtonPress() -> XYBluetoothResult { 61 | return self.subscribe(to: XYFinderPrimaryService.buttonState, delegate: (self.id, self)) 62 | } 63 | 64 | @discardableResult 65 | public override func unsubscribeToButtonPress(for referenceKey: UUID? = nil) -> XYBluetoothResult { 66 | return self.unsubscribe(from: XYFinderPrimaryService.buttonState, key: referenceKey?.uuidString ?? self.id) 67 | } 68 | 69 | @discardableResult 70 | public override func find(_ song: XYFinderSong = .findIt) -> XYBluetoothResult { 71 | let songData = Data(song.values(for: self.family)) 72 | return self.set(XYFinderPrimaryService.buzzer, value: XYBluetoothResult(data: songData)) 73 | } 74 | 75 | @discardableResult 76 | public override func stayAwake() -> XYBluetoothResult { 77 | return self.set(XYFinderPrimaryService.stayAwake, value: XYBluetoothResult(data: Data([0x01]))) 78 | } 79 | 80 | @discardableResult 81 | public override func isAwake() -> XYBluetoothResult { 82 | return self.get(XYFinderPrimaryService.stayAwake) 83 | } 84 | 85 | @discardableResult 86 | public override func fallAsleep() -> XYBluetoothResult { 87 | return self.set(XYFinderPrimaryService.stayAwake, value: XYBluetoothResult(data: Data([0x00]))) 88 | } 89 | 90 | @discardableResult 91 | public override func lock() -> XYBluetoothResult { 92 | return self.set(XYFinderPrimaryService.lock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_XY4)) 93 | } 94 | 95 | @discardableResult 96 | public override func unlock() -> XYBluetoothResult { 97 | return self.set(XYFinderPrimaryService.unlock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_XY4)) 98 | } 99 | 100 | @discardableResult 101 | public override func version() -> XYBluetoothResult { 102 | return self.get(DeviceInformationService.firmwareRevisionString) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XYBluetoothDeviceFactory.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // XYBluetoothDeviceFactort.swift 4 | // Pods-SampleiOS 5 | // 6 | // Created by Carter Harrison on 2/4/19. 7 | // 8 | 9 | import Foundation 10 | import CoreBluetooth 11 | 12 | public class XYBluetoothDeviceFactory { 13 | private static var uuidToCreators = [String : XYDeviceCreator]() 14 | internal static let deviceCache = XYDeviceCache() 15 | 16 | public static func addCreator (uuid : String, creator: XYDeviceCreator) { 17 | uuidToCreators[uuid.lowercased() ] = creator 18 | } 19 | 20 | public static func removeCreator (uuid: String) { 21 | uuidToCreators.removeValue(forKey: uuid.lowercased()) 22 | } 23 | 24 | public static var devices: [XYBluetoothDevice] { 25 | return deviceCache.devices.map { 26 | $1 27 | } 28 | } 29 | 30 | internal static func invalidateCache() { 31 | deviceCache.removeAll() 32 | } 33 | 34 | internal static func remove(device: XYBluetoothDevice) { 35 | self.deviceCache.remove(at: device.id) 36 | } 37 | 38 | public class func build(from xyId: String) -> XYBluetoothDevice? { 39 | guard let beacon = XYIBeaconDefinition.beacon(from: xyId) else { return nil } 40 | return self.build(from: beacon) 41 | } 42 | 43 | class func build(from peripheral: CBPeripheral) -> XYBluetoothDevice? { 44 | return devices.filter { 45 | $0.peripheral == peripheral 46 | }.first 47 | } 48 | 49 | public static func updateDeviceLocations(_ newLocation: XYLocationCoordinate2D) { 50 | devices.filter { $0.inRange }.forEach { 51 | ($0 as? XYFinderDevice)?.updateLocation(newLocation) 52 | } 53 | } 54 | 55 | // Create a device from an iBeacon definition, or update a cached device with the latest iBeacon/rssi data 56 | public class func build(from iBeacon: XYIBeaconDefinition, rssi: Int? = nil, updateRssiAndPower: Bool = false) -> XYBluetoothDevice? { 57 | guard let family = XYDeviceFamily.build(iBeacon: iBeacon) else { 58 | return nil 59 | } 60 | 61 | // Build or update 62 | var device: XYBluetoothDevice? 63 | if let foundDevice = deviceCache[iBeacon.xyId(from: family)] { 64 | device = foundDevice 65 | } else { 66 | device = uuidToCreators[iBeacon.uuid.uuidString.lowercased()]?.createFromIBeacon(iBeacon: iBeacon, rssi: rssi ?? XYDeviceProximity.defaultProximity) 67 | 68 | if let device = device { 69 | deviceCache[device.id] = device 70 | } 71 | } 72 | 73 | if updateRssiAndPower { 74 | // Update the device based on the read value if requested (typically when ranging beacons 75 | // to detect button presses and rssi changes) 76 | if ((rssi != nil ? rssi! : 0) < 0) { 77 | device?.update(rssi ?? XYDeviceProximity.defaultProximity, powerLevel: iBeacon.powerLevel) 78 | } 79 | } 80 | 81 | return device 82 | } 83 | 84 | public class func build (from family: XYDeviceFamily) -> XYBluetoothDevice? { 85 | let id = [family.prefix, family.uuid.uuidString.lowercased()].joined(separator: ":") 86 | let device = uuidToCreators[family.uuid.uuidString.lowercased()]?.createFromId(id: id) 87 | return device 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XYDeviceFamily.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYDeviceFamily.swift 3 | // Pods-SampleiOS 4 | // 5 | // Created by Carter Harrison on 2/4/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct XYDeviceFamily { 11 | static var famlies = [String : XYDeviceFamily]() 12 | 13 | public let uuid : UUID 14 | public let prefix : String 15 | public let familyName : String 16 | public let id : String 17 | 18 | public init(uuid: UUID, prefix : String, familyName : String, id : String) { 19 | self.uuid = uuid 20 | self.prefix = prefix 21 | self.familyName = familyName 22 | self.id = id 23 | } 24 | 25 | public func enable (enable : Bool) { 26 | if (enable) { 27 | XYDeviceFamily.famlies[self.uuid.uuidString.lowercased()] = self 28 | } else { 29 | XYDeviceFamily.famlies.removeValue(forKey: self.uuid.uuidString.lowercased()) 30 | } 31 | } 32 | 33 | public func diable () { 34 | XYDeviceFamily.famlies.removeValue(forKey: self.uuid.uuidString.lowercased()) 35 | } 36 | 37 | public static func allFamlies () -> [XYDeviceFamily] { 38 | return famlies.values.map { 39 | $0 40 | } 41 | } 42 | 43 | public static func build(iBeacon : XYIBeaconDefinition) -> XYDeviceFamily? { 44 | return XYDeviceFamily.famlies[iBeacon.uuid.uuidString.lowercased()] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XYGPSBluetoothDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYGPSBluetoothDevice.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/2/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | // The XYGPS-specific implementation 12 | public class XYGPSBluetoothDevice: XYFinderDeviceBase { 13 | public static let family = XYDeviceFamily.init(uuid: UUID(uuidString: XYGPSBluetoothDevice.uuid)!, 14 | prefix: XYGPSBluetoothDevice.prefix, 15 | familyName: XYGPSBluetoothDevice.familyName, 16 | id: XYGPSBluetoothDevice.id) 17 | 18 | public static let id = "GPS" 19 | public static let uuid : String = "9474f7c6-47a4-11e6-beb8-9e71128cae77" 20 | public static let familyName : String = "XY-GPS Finder" 21 | public static let prefix : String = "xy:gps" 22 | 23 | public var activated = false 24 | 25 | public init(_ id: String, iBeacon: XYIBeaconDefinition? = nil, rssi: Int = XYDeviceProximity.none.rawValue) { 26 | super.init(XYGPSBluetoothDevice.family, id: id, iBeacon: iBeacon, rssi: rssi) 27 | super.shouldCheckForButtonPressOnDetection = true 28 | } 29 | 30 | public convenience init(iBeacon: XYIBeaconDefinition, rssi: Int = XYDeviceProximity.none.rawValue) { 31 | self.init(iBeacon.xyId(from: XYGPSBluetoothDevice.family), iBeacon: iBeacon, rssi: rssi) 32 | } 33 | 34 | public override func subscribeToButtonPress() -> XYBluetoothResult { 35 | return self.subscribe(to: ControlService.button, delegate: (self.id, self)) 36 | } 37 | 38 | public override func unsubscribeToButtonPress(for referenceKey: UUID?) -> XYBluetoothResult { 39 | return self.unsubscribe(from: ControlService.button, key: referenceKey?.uuidString ?? self.id) 40 | } 41 | 42 | @discardableResult 43 | public override func find(_ song: XYFinderSong = .findIt) -> XYBluetoothResult { 44 | let songData = Data(song.values(for: self.family)) 45 | return self.set(ControlService.buzzerSelect, value: XYBluetoothResult(data: songData)) 46 | } 47 | 48 | @discardableResult 49 | public override func version() -> XYBluetoothResult { 50 | return self.get(ControlService.version) 51 | } 52 | 53 | @discardableResult 54 | public override func stayAwake() -> XYBluetoothResult { 55 | return self.set(ExtendedConfigService.registration, value: XYBluetoothResult(data: Data([0x01]))) 56 | } 57 | 58 | @discardableResult 59 | public override func fallAsleep() -> XYBluetoothResult { 60 | return self.set(ExtendedConfigService.registration, value: XYBluetoothResult(data: Data([0x00]))) 61 | } 62 | 63 | @discardableResult 64 | public override func lock() -> XYBluetoothResult { 65 | return self.set(BasicConfigService.lock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_DEFAULT)) 66 | } 67 | 68 | @discardableResult 69 | public override func unlock() -> XYBluetoothResult { 70 | return self.set(BasicConfigService.unlock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_DEFAULT)) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XYIBeaconDefinition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYIBeaconDefinition.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreLocation 11 | 12 | // A wrapper around the iBeacon advertisement data used by XY devices for ranging 13 | public struct XYIBeaconDefinition: Equatable { 14 | public let 15 | uuid: UUID, 16 | major: UInt16?, 17 | minor: UInt16? 18 | 19 | public var hasMajor: Bool { 20 | return major != nil && major! > 0 21 | } 22 | 23 | public var hasMinor: Bool { 24 | return minor != nil && minor! > 0 25 | } 26 | 27 | // Filters out the power level to generate a consistent minor value 28 | public func mainMinor(for family: XYDeviceFamily, slot: UInt16? = nil) -> UInt16? { 29 | guard let minor = self.minor else { return nil } 30 | switch family.id { 31 | case XY4BluetoothDevice.id, XY3BluetoothDevice.id, XY2BluetoothDevice.id, XYGPSBluetoothDevice.id: 32 | return (minor & 0xfff0) | (slot ?? 0x0004) 33 | default: 34 | return minor 35 | } 36 | } 37 | 38 | public var toString: String { 39 | var val = self.uuid.uuidString 40 | if let major = self.major { 41 | val += ".\(major)" 42 | } 43 | 44 | if let minor = self.minor { 45 | val += ".\(minor)" 46 | } 47 | 48 | return val 49 | } 50 | 51 | // Builds the beacon definition based on the uuid, major and minor 52 | public func xyId(from family: XYDeviceFamily) -> String { 53 | var xyid = [family.prefix, family.uuid.uuidString.lowercased()].joined(separator: ":") 54 | if let minor = mainMinor(for: family), let major = self.major { 55 | xyid.append(String(format: ".%ld.%ld", major, minor)) 56 | } else if let major = self.major { 57 | xyid.append(String(format: ".%ld", major)) 58 | } 59 | 60 | return xyid.lowercased() 61 | } 62 | 63 | public static func beacon(from xyId: String) -> XYIBeaconDefinition? { 64 | let parts = xyId.components(separatedBy: ":") 65 | 66 | if parts[safe: 1] == "near" { 67 | guard let uuid = UUID(uuidString: "00000000-0000-0000-0000-000000000000") else { return nil } 68 | return XYIBeaconDefinition(uuid: uuid, major: 0, minor: 0) 69 | } 70 | 71 | guard 72 | parts.count == 3, 73 | parts[safe: 0] == "xy" 74 | else { return nil } 75 | 76 | if parts[safe: 1] == "ibeacon" || parts[safe: 1] == "gps" || parts[safe: 1] == "mobiledevice" || parts[safe: 1] == "mobile" { 77 | guard 78 | let ids = parts[safe: 2]?.components(separatedBy: "."), 79 | let first = ids[safe: 0], let second = ids[safe: 1], let third = ids[safe: 2], 80 | let uuid = UUID(uuidString: first), 81 | let major = UInt16(second, radix: 10), 82 | let minor = UInt16(third, radix:10) 83 | else { return nil } 84 | 85 | return XYIBeaconDefinition(uuid: uuid, major: major, minor: minor) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // Determines the power value from the minor, changed when a user presses the button on the finder 92 | public var powerLevel: UInt8 { 93 | guard 94 | let minor = self.minor 95 | else { return UInt8(4) } 96 | return UInt8(minor & 0xf) 97 | } 98 | 99 | public static func ==(lhs: XYIBeaconDefinition, rhs: XYIBeaconDefinition) -> Bool { 100 | return lhs.toString == rhs.toString 101 | } 102 | } 103 | 104 | #if os(iOS) 105 | // MARK: CLBeacon Convenience 106 | extension CLBeacon { 107 | var xyiBeaconDefinition: XYIBeaconDefinition { 108 | return XYIBeaconDefinition( 109 | uuid: self.proximityUUID, 110 | major: self.major as? UInt16, 111 | minor: self.minor as? UInt16) 112 | } 113 | 114 | var family: XYDeviceFamily? { 115 | return XYDeviceFamily.build(iBeacon: self.xyiBeaconDefinition) 116 | } 117 | } 118 | 119 | // MARK: CLBeaconRegion Convenience 120 | extension CLBeaconRegion { 121 | var xyiBeaconDefinition: XYIBeaconDefinition { 122 | return XYIBeaconDefinition( 123 | uuid: self.proximityUUID, 124 | major: self.major as? UInt16, 125 | minor: self.minor as? UInt16) 126 | } 127 | 128 | var family: XYDeviceFamily? { 129 | return XYDeviceFamily.build(iBeacon: self.xyiBeaconDefinition) 130 | } 131 | } 132 | #endif 133 | 134 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Devices/XYMobileBluetoothDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYMobileBluetoothDevice.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/18/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | // The XY4-specific implementation 12 | public class XYMobileBluetoothDevice: XYFinderDeviceBase { 13 | public static let family = XYDeviceFamily.init(uuid: UUID(uuidString: XYMobileBluetoothDevice.uuid)!, 14 | prefix: XYMobileBluetoothDevice.prefix, 15 | familyName: XYMobileBluetoothDevice.familyName, 16 | id: XYMobileBluetoothDevice.id) 17 | 18 | public static let id = "MOBILE" 19 | public static let uuid : String = "735344c9-e820-42ec-9da7-f43a2b6802b9" 20 | public static let familyName : String = "Mobile Device" 21 | public static let prefix : String = "xy:mobiledevice" 22 | 23 | public init(_ id: String, iBeacon: XYIBeaconDefinition? = nil, rssi: Int = XYDeviceProximity.none.rawValue) { 24 | super.init(XYMobileBluetoothDevice.family, id: id, iBeacon: iBeacon, rssi: rssi) 25 | } 26 | 27 | public convenience init(iBeacon: XYIBeaconDefinition, rssi: Int = XYDeviceProximity.none.rawValue) { 28 | self.init(iBeacon.xyId(from: XYMobileBluetoothDevice.family), iBeacon: iBeacon, rssi: rssi) 29 | } 30 | 31 | @discardableResult 32 | public override func find(_ song: XYFinderSong = .findIt) -> XYBluetoothResult { 33 | let songData = Data(song.values(for: self.family)) 34 | return self.set(XYFinderPrimaryService.buzzer, value: XYBluetoothResult(data: songData)) 35 | } 36 | 37 | @discardableResult 38 | public override func stayAwake() -> XYBluetoothResult { 39 | return self.set(XYFinderPrimaryService.stayAwake, value: XYBluetoothResult(data: Data([0x01]))) 40 | } 41 | 42 | @discardableResult 43 | public override func fallAsleep() -> XYBluetoothResult { 44 | return self.set(XYFinderPrimaryService.stayAwake, value: XYBluetoothResult(data: Data([0x00]))) 45 | } 46 | 47 | @discardableResult 48 | public override func lock() -> XYBluetoothResult { 49 | return self.set(XYFinderPrimaryService.lock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_DEFAULT)) 50 | } 51 | 52 | @discardableResult 53 | public override func unlock() -> XYBluetoothResult { 54 | return self.set(XYFinderPrimaryService.unlock, value: XYBluetoothResult(data: XYConstants.DEVICE_LOCK_DEFAULT)) 55 | } 56 | 57 | @discardableResult 58 | public override func version() -> XYBluetoothResult { 59 | return self.get(DeviceInformationService.firmwareRevisionString) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Extensions/Array+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+extensions.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | public extension Collection { 10 | /// Returns the element at the specified index if it is within bounds, otherwise nil. 11 | subscript (safe index: Index) -> Element? { 12 | return indices.contains(index) ? self[index] : nil 13 | } 14 | } 15 | 16 | public extension String { 17 | var shortId: String { 18 | guard self.count > 0 else { return "" } 19 | var chunks = self.split(separator: ".") 20 | chunks.removeFirst() 21 | return chunks.joined(separator: ".") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Firmware/XYFirmwareLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYFirmwareLoader.swift 3 | // Pods-XyBleSdk_Example 4 | // 5 | // Created by Darren Sutherland on 11/27/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public class XYFirmwareLoader { 11 | 12 | public class func locateDocumentsFirmware() -> [URL] { 13 | // Get the document directory url 14 | guard let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return [] } 15 | 16 | var firmwareUrls = [URL]() 17 | 18 | do { 19 | // Get the directory contents urls (including subfolders urls) 20 | let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil, options: []) 21 | firmwareUrls = directoryContents.filter { $0.pathExtension == "img" || $0.pathExtension == "bin" } 22 | } catch { 23 | print(error.localizedDescription) 24 | } 25 | 26 | return firmwareUrls 27 | } 28 | 29 | public class func locateBundleFirmware(for fileName: String, type: String) -> URL? { 30 | let path: String? = Bundle.main.path(forResource: fileName, ofType: type) 31 | return URL.init(fileURLWithPath: path!) 32 | } 33 | 34 | public class func getFirmwareData(from url: URL) -> Data? { 35 | return try? Data(contentsOf: url) 36 | } 37 | 38 | public class func getFirmwareData(for path: String, success: @escaping (Data?) -> Void, error: @escaping (Error?) -> Void, progress: @escaping (Float) -> Void) { 39 | guard let loader = XYFirmwareRemoteLoader(path: path) else { 40 | error(XYBluetoothError.unableToUpdateFirmware) 41 | return 42 | } 43 | 44 | loader.download(success: success, error: error, progress: progress) 45 | } 46 | 47 | } 48 | 49 | // MARK: Wraps the remote version JSON data 50 | public struct XYRemoteVersionData: Decodable { 51 | public var version: String, path: String 52 | } 53 | 54 | public struct XYRemoteVersionData2: Decodable { 55 | public struct SentinelX: Decodable { 56 | public var version: String, path: String, type: String, bank: Int 57 | } 58 | 59 | public struct Firmware: Decodable { 60 | public var version: String, path: String, type: String, priority: Int, bank: Int 61 | } 62 | 63 | public struct Xy4: Decodable { 64 | public var firmware: [Firmware] 65 | } 66 | 67 | public var sentinelX: SentinelX 68 | public var xy4: Xy4 69 | } 70 | 71 | // MARK: Fetches the version JSON and the path to the firmware 72 | public class XYFirmwareRemoteVersionLoader { 73 | 74 | public init?(family: XYDeviceFamily) { 75 | guard family.id == XY4BluetoothDevice.id else { return nil } 76 | } 77 | 78 | public func get(from path: String = "https://s3.amazonaws.com/xyfirmware.xyo.network/sentinelx/version.json") -> XYRemoteVersionData? { 79 | guard 80 | let url = URL(string: path), 81 | let versionData = self.loadJson(from: url) else { 82 | return nil 83 | } 84 | 85 | return versionData 86 | } 87 | 88 | private func loadJson(from url: URL) -> XYRemoteVersionData? { 89 | do { 90 | let data = try Data(contentsOf: url) 91 | let decoder = JSONDecoder() 92 | let jsonData = try decoder.decode(XYRemoteVersionData.self, from: data) 93 | return jsonData 94 | } catch {} 95 | 96 | return nil 97 | } 98 | 99 | } 100 | 101 | // MARK: Fetches the version JSON and the path to the firmware 102 | public class XYSentinelFirmwareRemoteVersionLoader { 103 | 104 | public class func get(from path: String) -> XYRemoteVersionData2? { 105 | guard 106 | let url = URL(string: path), 107 | let versionData = self.loadJson(from: url) else { 108 | return nil 109 | } 110 | 111 | return versionData 112 | } 113 | 114 | private class func loadJson(from url: URL) -> XYRemoteVersionData2? { 115 | do { 116 | let data = try Data(contentsOf: url) 117 | let decoder = JSONDecoder() 118 | let jsonData = try decoder.decode(XYRemoteVersionData2.self, from: data) 119 | return jsonData 120 | } catch {} 121 | 122 | return nil 123 | } 124 | 125 | } 126 | 127 | // MARK: Used for grabbing the binary firmware from a remote server, allows for background download 128 | internal class XYFirmwareRemoteLoader: NSObject, URLSessionDownloadDelegate { 129 | 130 | private let url: URL 131 | private var backgroundSession: URLSession? 132 | 133 | private var isComplete: Bool = false 134 | 135 | private var 136 | success: ((Data?) -> Void)?, 137 | error: ((Error?) -> Void)?, 138 | progress: ((Float) -> Void)? 139 | 140 | init?(path: String) { 141 | guard let url = URL(string: path) else { return nil } 142 | self.url = url 143 | super.init() 144 | } 145 | 146 | func download(success: @escaping (Data?) -> Void, error: @escaping (Error?) -> Void, progress: @escaping (Float) -> Void) { 147 | self.success = success 148 | self.error = error 149 | self.progress = progress 150 | 151 | let sessionConfig = URLSessionConfiguration.background(withIdentifier: self.url.absoluteString) 152 | self.backgroundSession = Foundation.URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) 153 | guard let task = self.backgroundSession?.downloadTask(with: self.url) else { 154 | self.backgroundSession?.finishTasksAndInvalidate() 155 | self.error?(XYBluetoothError.unableToUpdateFirmware) 156 | return 157 | } 158 | task.resume() 159 | } 160 | 161 | // Download is done 162 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 163 | let data = try? Data(contentsOf: location) 164 | self.backgroundSession?.finishTasksAndInvalidate() 165 | self.isComplete = true 166 | self.success?(data) 167 | } 168 | 169 | // Download is in progress 170 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { 171 | let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) 172 | self.progress?(progress) 173 | } 174 | 175 | // Something bad happened 176 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 177 | if !isComplete { 178 | self.backgroundSession?.finishTasksAndInvalidate() 179 | self.error?(error) 180 | } 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/GattDescriptors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GattDescriptors.swift 3 | // Pods-XyBleSdk_Example 4 | // 5 | // Created by Darren Sutherland on 12/6/18. 6 | // 7 | 8 | import CoreBluetooth 9 | 10 | // A descriptor for a service, used as the key value for a dictionary in GattDeviceDescriptor 11 | public class GattServiceDescriptor: Hashable { 12 | public let 13 | uuid: CBUUID, 14 | name: String? 15 | 16 | init(_ service: CBService) { 17 | self.uuid = service.uuid 18 | self.name = GattDeviceDescriptor.definedServices.first(where: {$0.serviceUuid == service.uuid })?.serviceDisplayName 19 | } 20 | 21 | public func hash(into: inout Hasher) { 22 | return uuid.uuidString.hash(into: &into) 23 | } 24 | 25 | public static func == (lhs: GattServiceDescriptor, rhs: GattServiceDescriptor) -> Bool { 26 | return lhs.hashValue == rhs.hashValue 27 | } 28 | } 29 | 30 | // A descriptor for a characteristic, holding the service enum, pointer to the parent and the properties 31 | public struct GattCharacteristicDescriptor { 32 | public let 33 | uuid: CBUUID, 34 | parent: GattServiceDescriptor, 35 | properties: CBCharacteristicProperties, 36 | service: XYServiceCharacteristic? 37 | 38 | init(_ characteristic: CBCharacteristic, service: GattServiceDescriptor) { 39 | self.uuid = characteristic.uuid 40 | self.parent = service 41 | self.properties = characteristic.properties 42 | self.service = GattDeviceDescriptor.definedServices.first(where: {$0.characteristicUuid == characteristic.uuid}) 43 | } 44 | } 45 | 46 | // The dictionary container for all the services and respective characteristics that a device advertises 47 | public struct GattDeviceDescriptor { 48 | 49 | public let serviceCharacteristics: [GattServiceDescriptor: [GattCharacteristicDescriptor]] 50 | 51 | init(_ characteristics: [CBCharacteristic]) { 52 | let services = characteristics.map { GattServiceDescriptor($0.service) } 53 | 54 | self.serviceCharacteristics = characteristics.reduce(into: [GattServiceDescriptor: [GattCharacteristicDescriptor]](), { initial, characteristic in 55 | let service = services.first(where: { service in service.uuid == characteristic.service.uuid })! 56 | let descriptor = GattCharacteristicDescriptor(characteristic, service: service) 57 | return initial[service] == nil ? initial[service] = [descriptor] : initial[service]!.append(descriptor) 58 | }) 59 | } 60 | 61 | public var services: [GattServiceDescriptor] { 62 | return Array(self.serviceCharacteristics.keys) 63 | } 64 | 65 | // Array of all known services for easy building of the device descriptor 66 | internal static let definedServices: [XYServiceCharacteristic] = 67 | AlertNotificationService.values + 68 | BatteryService.values + 69 | CurrentTimeService.values + 70 | DeviceInformationService.values + 71 | GenericAccessService.values + 72 | GenericAttributeService.values + 73 | LinkLossService.values + 74 | OtaService.values + 75 | TxPowerService.values + 76 | BasicConfigService.values + 77 | ControlService.values + 78 | ExtendedConfigService.values + 79 | ExtendedControlService.values + 80 | XYFinderPrimaryService.values 81 | } 82 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/GattInquisitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GattInquisitor.swift 3 | // Pods-XyBleSdk_Example 4 | // 5 | // Created by Darren Sutherland on 12/5/18. 6 | // 7 | 8 | import CoreBluetooth 9 | import Promises 10 | 11 | // Used to inquire for all the characteristics of all the services the device has 12 | final class GattInquisitor: NSObject { 13 | 14 | fileprivate lazy var inquireServicesPromise = Promise<[CBService]>.pending() 15 | fileprivate lazy var inquireCharacteristicsPromise = Promise<[CBCharacteristic]>.pending() 16 | 17 | fileprivate let workQueue = DispatchQueue(label: "com.xyfindables.sdk.GattInquisitorQueue") 18 | 19 | fileprivate static let callTimeout: DispatchTimeInterval = .seconds(60) 20 | fileprivate static let queue = DispatchQueue(label:"com.xyfindables.sdk.XYGattInquisitorTimeoutQueue") 21 | fileprivate var timer: DispatchSourceTimer? 22 | 23 | fileprivate var device: XYBluetoothDevice? 24 | 25 | fileprivate let specifiedTimeout: DispatchTimeInterval 26 | 27 | fileprivate var disconnectSubKey: UUID? = nil 28 | 29 | public fileprivate(set) var status: GattRequestStatus = .disconnected 30 | 31 | public init(_ timeout: DispatchTimeInterval? = nil) { 32 | self.specifiedTimeout = timeout ?? GattInquisitor.callTimeout 33 | super.init() 34 | } 35 | 36 | func delegateKey(deviceUuid: UUID) -> String { 37 | return ["GI", deviceUuid.uuidString].joined(separator: ":") 38 | } 39 | 40 | @discardableResult public func inquire(for device: XYBluetoothDevice) -> Promise { 41 | let operationPromise = Promise.pending() 42 | guard 43 | let peripheral = device.peripheral, 44 | peripheral.state == .connected else { 45 | operationPromise.reject(XYBluetoothError.notConnected) 46 | return operationPromise 47 | } 48 | 49 | // If we disconnect at any point in the request, we stop the timeout and reject the promise 50 | self.disconnectSubKey = XYFinderDeviceEventManager.subscribe(to: [.disconnected]) { [weak self] event in 51 | XYFinderDeviceEventManager.unsubscribe(to: [.disconnected], referenceKey: self?.disconnectSubKey) 52 | guard let device = self?.device as? XYFinderDevice, device == event.device else { return } 53 | self?.timer = nil 54 | self?.status = .disconnected 55 | self?.inquireCharacteristicsPromise.reject(XYBluetoothError.peripheralDisconected(state: device.peripheral?.state)) 56 | } 57 | 58 | print("START Inquire: \(device.id.shortId)") 59 | 60 | // Create timeout using the operation queue. Self-cleaning if we timeout 61 | timer = DispatchSource.makeTimerSource(queue: GattInquisitor.queue) 62 | timer?.schedule(deadline: DispatchTime.now() + self.specifiedTimeout) 63 | timer?.setEventHandler(handler: { [weak self] in 64 | guard let s = self else { return } 65 | print("TIMEOUT Inquire: \(device.id.shortId)") 66 | s.timer = nil 67 | s.status = .timedOut 68 | s.inquireCharacteristicsPromise.reject(XYBluetoothError.timedOut) 69 | }) 70 | timer?.resume() 71 | 72 | var characteristics = [CBCharacteristic]() 73 | 74 | self.device = device 75 | device.subscribe(self, key: self.delegateKey(deviceUuid: peripheral.identifier)) 76 | 77 | // Using an await-style promise, ask for each set of characteristics from the services 78 | // If we are not on the final service, we re-up the inquire promise 79 | self.inquireServices(device).then(on: self.workQueue) { services in 80 | for service in services { 81 | characteristics += try await(self.inquireCharacteristics(device, service: service)) 82 | if services.last != service { 83 | self.inquireCharacteristicsPromise = Promise<[CBCharacteristic]>.pending() 84 | } 85 | } 86 | }.then(on: self.workQueue) { _ in 87 | operationPromise.fulfill(GattDeviceDescriptor(characteristics)) 88 | }.always(on: self.workQueue) { 89 | device.unsubscribe(for: self.delegateKey(deviceUuid: peripheral.identifier)) 90 | self.timer = nil 91 | XYFinderDeviceEventManager.unsubscribe(to: [.disconnected], referenceKey: self.disconnectSubKey) 92 | print("ALWAYS Inquire: \(device.id.shortId)") 93 | }.catch(on: self.workQueue) { error in 94 | operationPromise.reject(error) 95 | } 96 | 97 | return operationPromise 98 | } 99 | 100 | } 101 | 102 | fileprivate extension GattInquisitor { 103 | 104 | @discardableResult func inquireServices(_ device: XYBluetoothDevice) -> Promise<[CBService]> { 105 | guard 106 | self.status != .timedOut, 107 | let peripheral = device.peripheral, 108 | peripheral.state == .connected 109 | else { 110 | self.inquireServicesPromise.reject(XYBluetoothError.notConnected) 111 | return self.inquireServicesPromise 112 | } 113 | 114 | self.status = .discoveringServices 115 | peripheral.discoverServices(nil) 116 | 117 | return self.inquireServicesPromise 118 | } 119 | 120 | func inquireCharacteristics(_ device: XYBluetoothDevice, service: CBService) -> Promise<[CBCharacteristic]> { 121 | guard 122 | self.status != .timedOut, 123 | let peripheral = device.peripheral, 124 | peripheral.state == .connected 125 | else { 126 | self.inquireCharacteristicsPromise.reject(XYBluetoothError.notConnected) 127 | return self.inquireCharacteristicsPromise 128 | } 129 | 130 | self.status = .discoveringCharacteristics 131 | peripheral.discoverCharacteristics(nil, for: service) 132 | 133 | return self.inquireCharacteristicsPromise 134 | } 135 | 136 | } 137 | 138 | extension GattInquisitor: CBPeripheralDelegate { 139 | 140 | private func serviceCharacteristicDelegateValidation(_ peripheral: CBPeripheral, error: Error?) -> Bool { 141 | guard self.status != .disconnected || self.status != .timedOut else { return false } 142 | 143 | guard error == nil else { 144 | self.inquireCharacteristicsPromise.reject(XYBluetoothError.cbPeripheralDelegateError(error!)) 145 | return false 146 | } 147 | 148 | guard 149 | self.device?.peripheral == peripheral 150 | else { 151 | self.inquireCharacteristicsPromise.reject(XYBluetoothError.mismatchedPeripheral) 152 | return false 153 | } 154 | 155 | return true 156 | } 157 | 158 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 159 | workQueue.async { 160 | guard self.serviceCharacteristicDelegateValidation(peripheral, error: error) else { return } 161 | 162 | guard let services = peripheral.services else { 163 | self.inquireServicesPromise.reject(XYBluetoothError.serviceNotFound) 164 | return 165 | } 166 | 167 | self.inquireServicesPromise.fulfill(services) 168 | } 169 | } 170 | 171 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 172 | guard 173 | let characteristics = service.characteristics 174 | else { 175 | self.inquireCharacteristicsPromise.reject(XYBluetoothError.characteristicNotFound) 176 | return 177 | } 178 | 179 | self.inquireCharacteristicsPromise.fulfill(characteristics) 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Peripheral/XYCBPeripheralManager .swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYCBPeripheralManager .swift 3 | // XyBleSdk 4 | // 5 | // Created by Carter Harrison on 2/18/19. 6 | // 7 | 8 | import Foundation 9 | import CoreBluetooth 10 | import CoreLocation 11 | import Promises 12 | 13 | 14 | open class XYCBPeripheralManager : NSObject, CBPeripheralManagerDelegate { 15 | private var services = [String : XYMutableService]() 16 | private var turnOnPromise : Promise? = nil 17 | private var manager : CBPeripheralManager? = nil 18 | public static let instance = XYCBPeripheralManager() 19 | 20 | public func addService (service: XYMutableService) { 21 | services[service.cbService.uuid.uuidString] = service 22 | manager?.add(service.cbService) 23 | } 24 | 25 | public func removeService (service : XYMutableService) { 26 | guard let service = services[service.cbService.uuid.uuidString] else { 27 | return 28 | } 29 | 30 | manager?.remove(service.cbService) 31 | services.removeValue(forKey: service.cbService.uuid.uuidString) 32 | } 33 | 34 | public func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { 35 | turnOnPromise?.fulfill(peripheral.state == .poweredOn) 36 | } 37 | 38 | public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) { 39 | guard let service = services[request.characteristic.service.uuid.uuidString] else { 40 | return 41 | } 42 | 43 | service.handleReadRequest(request, peripheral: peripheral) 44 | } 45 | 46 | public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { 47 | for i in 0...requests.count - 1 { 48 | guard let service = services[requests[i].characteristic.service.uuid.uuidString] else { 49 | return 50 | } 51 | 52 | service.handleWriteRequest(requests[i], peripheral: peripheral) 53 | } 54 | } 55 | 56 | public func turnOn () -> Promise { 57 | manager = CBPeripheralManager(delegate: self, queue: XYCentral.centralQueue) 58 | let promise = Promise.pending() 59 | turnOnPromise = promise 60 | return promise 61 | } 62 | 63 | public func turnOff () { 64 | manager?.stopAdvertising() 65 | manager = nil 66 | } 67 | 68 | @available(OSX 10.15, *) 69 | public func startAdvertiseing (advertisementUUIDs: [CBUUID]?, deviceName: String?, beacon : CLBeaconRegion?) { 70 | let peripheralData = beacon?.peripheralData(withMeasuredPower: nil) 71 | var adDataMap = ((peripheralData as? [String : Any]) ?? [String : Any]()) 72 | 73 | adDataMap[CBAdvertisementDataServiceUUIDsKey] = advertisementUUIDs 74 | adDataMap[CBAdvertisementDataLocalNameKey] = deviceName 75 | self.manager?.startAdvertising(adDataMap) 76 | } 77 | 78 | public func stopAdvetrtising () { 79 | manager?.stopAdvertising() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Peripheral/XYMutableCharacteristic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYMutableCharacteristic.swift 3 | // XyBleSdk 4 | // 5 | // Created by Carter Harrison on 2/19/19. 6 | // 7 | 8 | import Foundation 9 | import CoreBluetooth 10 | 11 | public protocol XYMutableCharacteristic { 12 | var cbCharacteristic : CBMutableCharacteristic { get } 13 | 14 | func handleReadRequest(_ request: CBATTRequest, peripheral: CBPeripheralManager) 15 | func handleWriteRequest(_ request: CBATTRequest, peripheral: CBPeripheralManager) 16 | func handleSubscribeToCharacteristic(peripheral: CBPeripheralManager) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Peripheral/XYMutableService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYMutableService.swift 3 | // XyBleSdk 4 | // 5 | // Created by Carter Harrison on 2/19/19. 6 | // 7 | 8 | import Foundation 9 | import CoreBluetooth 10 | 11 | open class XYMutableService { 12 | public var characteristics = [String : XYMutableCharacteristic]() 13 | public let cbService : CBMutableService 14 | 15 | public init (cbService: CBMutableService) { 16 | self.cbService = cbService 17 | cbService.characteristics = [] 18 | } 19 | 20 | public func addCharacteristic (characteristic: XYMutableCharacteristic) { 21 | characteristics[characteristic.cbCharacteristic.uuid.uuidString] = characteristic 22 | cbService.characteristics?.append(characteristic.cbCharacteristic) 23 | } 24 | 25 | public func removeCharacteristic (characteristic : XYMutableCharacteristic) { 26 | characteristics.removeValue(forKey: characteristic.cbCharacteristic.uuid.uuidString) 27 | guard let i = cbService.characteristics?.lastIndex(where: { (n) -> Bool in 28 | n.uuid.uuidString == characteristic.cbCharacteristic.uuid.uuidString 29 | }) else { 30 | return 31 | } 32 | 33 | cbService.characteristics?.remove(at: i) 34 | } 35 | 36 | public func removeCharacteristics() { 37 | characteristics.removeAll() 38 | cbService.characteristics?.removeAll() 39 | } 40 | 41 | open func handleReadRequest(_ request: CBATTRequest, peripheral: CBPeripheralManager) { 42 | characteristics[request.characteristic.uuid.uuidString]?.handleReadRequest(request, peripheral: peripheral) 43 | } 44 | 45 | open func handleWriteRequest(_ request: CBATTRequest, peripheral: CBPeripheralManager) { 46 | characteristics[request.characteristic.uuid.uuidString]?.handleWriteRequest(request, peripheral: peripheral) 47 | } 48 | 49 | open func handleSubscribeToCharacteristic(characteristic: CBMutableCharacteristic, peripheral: CBPeripheralManager) { 50 | characteristics[characteristic.uuid.uuidString]?.handleSubscribeToCharacteristic(peripheral: peripheral) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/AlertNotificationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertNotificationService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/14/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum AlertNotificationService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Alert Notification" } 14 | public var serviceUuid: CBUUID { return AlertNotificationService.serviceUuid } 15 | 16 | case controlPoint 17 | case unreadAlertStatus 18 | case newAlert 19 | case supportedNewAlertCategory 20 | case supportedUnreadAlertCategory 21 | 22 | public var characteristicUuid: CBUUID { 23 | return AlertNotificationService.uuids[self]! 24 | } 25 | 26 | public var characteristicType: XYServiceCharacteristicType { 27 | return .integer 28 | } 29 | 30 | public var displayName: String { 31 | switch self { 32 | case .controlPoint: return "Control Point" 33 | case .unreadAlertStatus: return "Unread Alert Status" 34 | case .newAlert: return "New Alert" 35 | case .supportedNewAlertCategory: return "Supported New Alert Category" 36 | case .supportedUnreadAlertCategory: return "Supported Unread Alert Category" 37 | } 38 | } 39 | 40 | private static let serviceUuid = CBUUID(string: "00001811-0000-1000-8000-00805F9B34FB") 41 | 42 | private static let uuids: [AlertNotificationService: CBUUID] = [ 43 | controlPoint: CBUUID(string: "00002a44-0000-1000-8000-00805f9b34fb"), 44 | unreadAlertStatus: CBUUID(string: "00002a45-0000-1000-8000-00805f9b34fb"), 45 | newAlert: CBUUID(string: "00002a46-0000-1000-8000-00805f9b34fb"), 46 | supportedNewAlertCategory: CBUUID(string: "00002a47-0000-1000-8000-00805f9b34fb"), 47 | supportedUnreadAlertCategory: CBUUID(string: "00002a48-0000-1000-8000-00805f9b34fb") 48 | ] 49 | 50 | public static var values: [XYServiceCharacteristic] = [ 51 | controlPoint, unreadAlertStatus, newAlert, supportedNewAlertCategory, supportedUnreadAlertCategory 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/BatteryService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BatteryService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/14/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum BatteryService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Battery" } 14 | public var serviceUuid: CBUUID { return BatteryService.serviceUuid } 15 | 16 | case level 17 | 18 | public var characteristicUuid: CBUUID { 19 | return BatteryService.uuids[self]! 20 | } 21 | 22 | public var characteristicType: XYServiceCharacteristicType { 23 | return .integer 24 | } 25 | 26 | public var displayName: String { 27 | return "Battery Level" 28 | } 29 | 30 | private static let serviceUuid = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB") 31 | 32 | private static let uuids: [BatteryService: CBUUID] = [ 33 | .level: CBUUID(string: "00002a19-0000-1000-8000-00805f9b34fb") 34 | ] 35 | 36 | public static var values: [XYServiceCharacteristic] = [ 37 | level 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/CurrentTimeService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentTimeService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum CurrentTimeService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Current Time" } 14 | public var serviceUuid: CBUUID { return CurrentTimeService.serviceUuid } 15 | 16 | case currentTime 17 | case localTimeInformation 18 | case referenceTimeInformation 19 | 20 | public var characteristicUuid: CBUUID { 21 | return CurrentTimeService.uuids[self]! 22 | } 23 | 24 | public var characteristicType: XYServiceCharacteristicType { 25 | return .integer 26 | } 27 | 28 | public var displayName: String { 29 | switch self { 30 | case.currentTime: return "Current Time" 31 | case .localTimeInformation: return "Local Time Information" 32 | case .referenceTimeInformation: return "Reference Time Information" 33 | } 34 | } 35 | 36 | private static let serviceUuid = CBUUID(string: "00001805-0000-1000-8000-00805F9B34FB") 37 | 38 | private static let uuids: [CurrentTimeService: CBUUID] = [ 39 | .currentTime : CBUUID(string: "00002a2b-0000-1000-8000-00805f9b34fb"), 40 | .localTimeInformation : CBUUID(string: "00002a0f-0000-1000-8000-00805f9b34fb"), 41 | .referenceTimeInformation : CBUUID(string: "00002a14-0000-1000-8000-00805f9b34fb") 42 | ] 43 | 44 | public static var values: [XYServiceCharacteristic] = [ 45 | currentTime, localTimeInformation, referenceTimeInformation 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/DeviceInformationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceInformationService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/12/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum DeviceInformationService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Device Information" } 14 | public var serviceUuid: CBUUID { return DeviceInformationService.serviceUuid } 15 | 16 | case systemId 17 | case firmwareRevisionString 18 | case modelNumberString 19 | case serialNumberString 20 | case hardwareRevisionString 21 | case softwareRevisionString 22 | case manufacturerNameString 23 | case ieeeRegulatoryCertificationDataList 24 | case pnpId 25 | 26 | public var characteristicUuid: CBUUID { 27 | return DeviceInformationService.uuids[self]! 28 | } 29 | 30 | public var characteristicType: XYServiceCharacteristicType { 31 | switch self { 32 | case .systemId, .ieeeRegulatoryCertificationDataList, .pnpId: 33 | return .integer 34 | case .firmwareRevisionString, .hardwareRevisionString, .manufacturerNameString, .modelNumberString, .serialNumberString, .softwareRevisionString: 35 | return .string 36 | } 37 | } 38 | 39 | public var displayName: String { 40 | switch self { 41 | case .systemId: return "System Id" 42 | case .firmwareRevisionString: return "Firmware Revision" 43 | case .modelNumberString: return "Model Number" 44 | case .serialNumberString: return "Serial Number" 45 | case .hardwareRevisionString: return "Hardware Revision" 46 | case .softwareRevisionString: return "Software Revision" 47 | case .manufacturerNameString: return "Manufacturer Name" 48 | case .ieeeRegulatoryCertificationDataList: return "IEEE Regulatory Certification Data List" 49 | case .pnpId: return "PnP Id" 50 | } 51 | } 52 | 53 | private static let serviceUuid = CBUUID(string: "0000180A-0000-1000-8000-00805F9B34FB") 54 | 55 | private static let uuids: [DeviceInformationService: CBUUID] = [ 56 | .systemId: CBUUID(string: "00002a23-0000-1000-8000-00805f9b34fb"), 57 | .firmwareRevisionString: CBUUID(string: "00002a26-0000-1000-8000-00805f9b34fb"), 58 | .modelNumberString: CBUUID(string: "00002a24-0000-1000-8000-00805f9b34fb"), 59 | .serialNumberString: CBUUID(string: "00002a25-0000-1000-8000-00805f9b34fb"), 60 | .hardwareRevisionString: CBUUID(string: "00002a27-0000-1000-8000-00805f9b34fb"), 61 | .softwareRevisionString: CBUUID(string: "00002a28-0000-1000-8000-00805f9b34fb"), 62 | .manufacturerNameString: CBUUID(string: "00002a29-0000-1000-8000-00805f9b34fb"), 63 | .ieeeRegulatoryCertificationDataList: CBUUID(string: "00002a2a-0000-1000-8000-00805f9b34fb"), 64 | .pnpId: CBUUID(string: "00002a50-0000-1000-8000-00805f9b34fb") 65 | ] 66 | 67 | public static var values: [XYServiceCharacteristic] = [ 68 | systemId, firmwareRevisionString, modelNumberString, serialNumberString, hardwareRevisionString, softwareRevisionString, manufacturerNameString, ieeeRegulatoryCertificationDataList, pnpId 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/GenericAccessService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericAccessService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/19/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum GenericAccessService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Generic Access" } 14 | public var serviceUuid: CBUUID { return GenericAccessService.serviceUuid } 15 | 16 | case deviceName 17 | case appearance 18 | case privacyFlag 19 | case reconnectionAddress 20 | case peripheralPreferredConnectionParameters 21 | 22 | public var characteristicUuid: CBUUID { 23 | return GenericAccessService.uuids[self]! 24 | } 25 | 26 | public var characteristicType: XYServiceCharacteristicType { 27 | return .integer 28 | } 29 | 30 | public var displayName: String { 31 | switch self { 32 | case .deviceName: return "Device Name" 33 | case .appearance: return "Apperance" 34 | case .privacyFlag: return "Privacy Flag" 35 | case .reconnectionAddress: return "Reconnection Address" 36 | case .peripheralPreferredConnectionParameters: return "Peripheral Preferred Connection Parameters" 37 | } 38 | } 39 | 40 | private static let serviceUuid = CBUUID(string: "00001800-0000-1000-8000-00805F9B34FB") 41 | 42 | private static let uuids: [GenericAccessService: CBUUID] = [ 43 | .deviceName : CBUUID(string: "00002a00-0000-1000-8000-00805f9b34fb"), 44 | .appearance : CBUUID(string: "00002a01-0000-1000-8000-00805f9b34fb"), 45 | .privacyFlag : CBUUID(string: "00002a02-0000-1000-8000-00805f9b34fb"), 46 | .reconnectionAddress : CBUUID(string: "00002a03-0000-1000-8000-00805f9b34fb"), 47 | .peripheralPreferredConnectionParameters : CBUUID(string: "00002a04-0000-1000-8000-00805f9b34fb") 48 | ] 49 | 50 | public static var values: [XYServiceCharacteristic] = [ 51 | deviceName, appearance, privacyFlag, reconnectionAddress, peripheralPreferredConnectionParameters 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/GenericAttributeService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericAttributeService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum GenericAttributeService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Generic Attribute" } 14 | public var serviceUuid: CBUUID { return GenericAttributeService.serviceUuid } 15 | 16 | case serviceChanged 17 | 18 | public var characteristicUuid: CBUUID { 19 | return GenericAttributeService.uuids[self]! 20 | } 21 | 22 | public var characteristicType: XYServiceCharacteristicType { 23 | return .integer 24 | } 25 | 26 | public var displayName: String { 27 | return "Service Changed" 28 | } 29 | 30 | private static let serviceUuid = CBUUID(string: "00001801-0000-1000-8000-00805F9B34FB") 31 | 32 | private static let uuids: [GenericAttributeService: CBUUID] = [ 33 | .serviceChanged : CBUUID(string: "00002a05-0000-1000-8000-00805f9b34fb") 34 | ] 35 | 36 | public static var values: [XYServiceCharacteristic] = [ 37 | serviceChanged 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/LinkLossService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkLossService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum LinkLossService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Link Loss" } 14 | public var serviceUuid: CBUUID { return LinkLossService.serviceUuid } 15 | 16 | case alertLevel 17 | 18 | public var characteristicUuid: CBUUID { 19 | return LinkLossService.uuids[self]! 20 | } 21 | 22 | public var characteristicType: XYServiceCharacteristicType { 23 | return .integer 24 | } 25 | 26 | public var displayName: String { 27 | return "Alert Level" 28 | } 29 | 30 | private static let serviceUuid = CBUUID(string: "00001803-0000-1000-8000-00805F9B34FB") 31 | 32 | private static let uuids: [LinkLossService: CBUUID] = [ 33 | .alertLevel : CBUUID(string: "00002a06-0000-1000-8000-00805f9b34fb") 34 | ] 35 | 36 | public static var values: [XYServiceCharacteristic] = [ 37 | alertLevel 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/OtaService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtaService.swift 3 | // XYSdkSample 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 Darren Sutherland. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum OtaService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "OTA" } 14 | public var serviceUuid: CBUUID { return OtaService.serviceUuid } 15 | 16 | case memDev 17 | case gpioMap 18 | case memInfo 19 | case patchLen 20 | case patchData 21 | case servStatus 22 | 23 | public var characteristicUuid: CBUUID { 24 | return OtaService.uuids[self]! 25 | } 26 | 27 | public var characteristicType: XYServiceCharacteristicType { 28 | return .integer 29 | } 30 | 31 | public var displayName: String { 32 | switch self { 33 | case .memDev: return "memDev" 34 | case .gpioMap: return "gpioMap" 35 | case .memInfo: return "memInfo" 36 | case .patchLen: return "patchLen" 37 | case .patchData: return "patchData" 38 | case .servStatus: return "servStatus" 39 | } 40 | } 41 | 42 | private static let serviceUuid = CBUUID(string: "0000fef5-0000-1000-8000-00805f9b34fb") 43 | 44 | private static let uuids: [OtaService: CBUUID] = [ 45 | .memDev : CBUUID(string: "8082caa8-41a6-4021-91c6-56f9b954cc34"), 46 | .gpioMap : CBUUID(string: "724249f0-5eC3-4b5f-8804-42345af08651"), 47 | .memInfo : CBUUID(string: "6c53db25-47a1-45fe-a022-7c92fb334fd4"), 48 | .patchLen : CBUUID(string: "9d84b9a3-000c-49d8-9183-855b673fda31"), 49 | .patchData : CBUUID(string: "457871e8-d516-4ca1-9116-57d0b17b9cb2"), 50 | .servStatus : CBUUID(string: "5f78df94-798c-46f5-990a-b3eb6a065c88") 51 | ] 52 | 53 | public static var values: [XYServiceCharacteristic] = [ 54 | memDev, gpioMap, memInfo, patchLen, patchData, servStatus 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/Standard/TxPowerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TxPowerService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/19/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum TxPowerService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Tx Power" } 14 | public var serviceUuid: CBUUID { return TxPowerService.serviceUuid } 15 | 16 | case txPowerLevel 17 | 18 | public var characteristicUuid: CBUUID { 19 | return TxPowerService.uuids[self]! 20 | } 21 | 22 | public var characteristicType: XYServiceCharacteristicType { 23 | return .integer 24 | } 25 | 26 | public var displayName: String { 27 | return "Tx Power Level" 28 | } 29 | 30 | private static let serviceUuid = CBUUID(string: "00001800-0000-1000-8000-00805F9B34FB") 31 | 32 | private static let uuids: [TxPowerService: CBUUID] = [ 33 | .txPowerLevel: CBUUID(string: "00002a07-0000-1000-8000-00805f9b34fb") 34 | ] 35 | 36 | public static var values: [XYServiceCharacteristic] = [ 37 | txPowerLevel 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/XYServiceCharacteristic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Service.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | import Promises 11 | 12 | // Used for proper upacking of the data result from reading characteristcs 13 | public enum XYServiceCharacteristicType { 14 | case string 15 | case integer 16 | case byte 17 | } 18 | 19 | // Protocol which defines a service and a characteristic, implemented as enumerations in the various *Service files 20 | public protocol XYServiceCharacteristic { 21 | var serviceDisplayName: String { get } 22 | var serviceUuid: CBUUID { get } 23 | var characteristicUuid: CBUUID { get } 24 | var characteristicType: XYServiceCharacteristicType { get } 25 | var displayName: String { get } 26 | 27 | static var values: [XYServiceCharacteristic] { get } 28 | } 29 | 30 | // Global methods for all service characteristics, these create a disposable GattRequest to handle the 31 | // service and characteristic discovery and the setting or getting of a characteristic. 32 | public extension XYServiceCharacteristic { 33 | 34 | var characteristics: [XYServiceCharacteristic] { 35 | return type(of: self).values 36 | } 37 | 38 | func get(from device: XYBluetoothDevice, timeout: DispatchTimeInterval? = nil) -> Promise { 39 | return GattRequest(self, timeout: timeout).get(from: device).then { value in 40 | XYBluetoothResult(data: value) 41 | } 42 | } 43 | 44 | func set(to device: XYBluetoothDevice, value: XYBluetoothResult, timeout: DispatchTimeInterval? = nil, withResponse: Bool = true) -> Promise { 45 | return GattRequest(self, timeout: timeout).set(to: device, valueObj: value, withResponse: withResponse) 46 | } 47 | 48 | func notify(for device: XYBluetoothDevice, enabled: Bool, timeout: DispatchTimeInterval? = nil) -> Promise { 49 | return GattRequest(self, timeout: timeout).notify(for: device, enabled: enabled) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/xy3/BasicConfigService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicConfigService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum BasicConfigService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Basic Config" } 14 | public var serviceUuid: CBUUID { return BasicConfigService.serviceUuid } 15 | 16 | case lockStatus 17 | case lock 18 | case unlock 19 | case uuid 20 | case major 21 | case minor 22 | case interval 23 | case otaWrite 24 | case reboot 25 | 26 | private static let serviceUuid = CBUUID(string: "F014EE00-0439-3000-E001-00001001FFFF") 27 | 28 | public var characteristicUuid: CBUUID { 29 | return BasicConfigService.uuids[self]! 30 | } 31 | 32 | public var characteristicType: XYServiceCharacteristicType { 33 | switch self { 34 | case .lockStatus, .major, .minor, .reboot: 35 | return .integer 36 | default: 37 | return .byte 38 | } 39 | } 40 | 41 | public var displayName: String { 42 | switch self { 43 | case .lockStatus: return "Lock Status" 44 | case .lock: return "Lock" 45 | case .unlock: return "Unlock" 46 | case .uuid: return "UUID" 47 | case .major: return "Major" 48 | case .minor: return "Minor" 49 | case .interval: return "Interval" 50 | case .otaWrite: return "OTA Write" 51 | case .reboot: return "Reboot" 52 | } 53 | } 54 | 55 | private static let uuids: [BasicConfigService: CBUUID] = [ 56 | lockStatus : CBUUID(string: "F014EE01-0439-3000-E001-00001001FFFF"), 57 | lock : CBUUID(string: "F014EE02-0439-3000-E001-00001001FFFF"), 58 | unlock : CBUUID(string: "F014EE03-0439-3000-E001-00001001FFFF"), 59 | uuid : CBUUID(string: "F014EE04-0439-3000-E001-00001001FFFF"), 60 | major : CBUUID(string: "F014EE05-0439-3000-E001-00001001FFFF"), 61 | minor : CBUUID(string: "F014EE06-0439-3000-E001-00001001FFFF"), 62 | interval : CBUUID(string: "F014EE07-0439-3000-E001-00001001FFFF"), 63 | otaWrite : CBUUID(string: "F014EE09-0439-3000-E001-00001001FFFF"), 64 | reboot : CBUUID(string: "F014EE0A-0439-3000-E001-00001001FFFF") 65 | ] 66 | 67 | public static var values: [XYServiceCharacteristic] = [ 68 | lockStatus, lock, unlock, uuid, major, minor, interval, otaWrite, reboot 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/xy3/ControlService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControlService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum ControlService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Control" } 14 | public var serviceUuid: CBUUID { return ControlService.serviceUuid } 15 | 16 | case buzzer 17 | case handshake 18 | case version 19 | case buzzerSelect 20 | case surge 21 | case button 22 | case disconnect 23 | 24 | private static let serviceUuid = CBUUID(string: "F014ED15-0439-3000-E001-00001001FFFF") 25 | 26 | public var characteristicUuid: CBUUID { 27 | return ControlService.uuids[self]! 28 | } 29 | 30 | public var characteristicType: XYServiceCharacteristicType { 31 | switch self { 32 | case .buzzer, .handshake, .buzzerSelect, .surge, .button, .disconnect: 33 | return .integer 34 | case .version: 35 | return .string 36 | } 37 | } 38 | 39 | public var displayName: String { 40 | switch self { 41 | case .buzzer: return "Buzzer" 42 | case .handshake: return "Handshake" 43 | case .version: return "Version" 44 | case .buzzerSelect: return "Buzzer Select" 45 | case .surge: return "Surge" 46 | case .button: return "Button" 47 | case .disconnect: return "Disconnect" 48 | } 49 | } 50 | 51 | private static let uuids: [ControlService: CBUUID] = [ 52 | buzzer : CBUUID(string: "F014FFF1-0439-3000-E001-00001001FFFF"), 53 | handshake : CBUUID(string: "F014FFF2-0439-3000-E001-00001001FFFF"), 54 | version : CBUUID(string: "F014FFF4-0439-3000-E001-00001001FFFF"), 55 | buzzerSelect : CBUUID(string: "F014FFF6-0439-3000-E001-00001001FFFF"), 56 | surge : CBUUID(string: "F014FFF7-0439-3000-E001-00001001FFFF"), 57 | button : CBUUID(string: "F014FFF8-0439-3000-E001-00001001FFFF"), 58 | disconnect : CBUUID(string: "F014FFF9-0439-3000-E001-00001001FFFF") 59 | ] 60 | 61 | public static var values: [XYServiceCharacteristic] = [ 62 | buzzer, handshake, version, buzzerSelect, surge, button, disconnect 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/xy3/ExtendedConfigService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtendedConfigService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/2/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum ExtendedConfigService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Extended Config" } 14 | public var serviceUuid: CBUUID { return ExtendedConfigService.serviceUuid } 15 | 16 | case virtualBeaconSettings 17 | case tone 18 | case registration 19 | case inactiveVirtualBeaconSettings 20 | case inactiveInterval 21 | case gpsInterval 22 | case gpsMode 23 | case simId 24 | 25 | private static let serviceUuid = CBUUID(string: "F014FF00-0439-3000-E001-00001001FFFF") 26 | 27 | public var characteristicUuid: CBUUID { 28 | return ExtendedConfigService.uuids[self]! 29 | } 30 | 31 | public var characteristicType: XYServiceCharacteristicType { 32 | return .integer 33 | } 34 | 35 | public var displayName: String { 36 | switch self { 37 | case .virtualBeaconSettings: return "Virtual Beacon Settings" 38 | case .tone: return "Tone" 39 | case .registration: return "Registration" 40 | case .inactiveVirtualBeaconSettings: return "Inactive Virtual Beacon Settings" 41 | case .inactiveInterval: return "Inactive Interval" 42 | case .gpsInterval: return "GPS Interval" 43 | case .gpsMode: return "GPS Mode" 44 | case .simId: return "SIM Id" 45 | } 46 | } 47 | 48 | private static let uuids: [ExtendedConfigService: CBUUID] = [ 49 | virtualBeaconSettings : CBUUID(string: "F014FF02-0439-3000-E001-00001001FFFF"), 50 | tone : CBUUID(string: "F014FF03-0439-3000-E001-00001001FFFF"), 51 | registration : CBUUID(string: "F014FF05-0439-3000-E001-00001001FFFF"), 52 | inactiveVirtualBeaconSettings : CBUUID(string: "F014FF06-0439-3000-E001-00001001FFFF"), 53 | inactiveInterval : CBUUID(string: "F014FF07-0439-3000-E001-00001001FFFF"), 54 | gpsInterval : CBUUID(string: "2ABBAA00-0439-3000-E001-00001001FFFF"), 55 | gpsMode : CBUUID(string: "2A99AA00-0439-3000-E001-00001001FFFF"), 56 | simId : CBUUID(string: "2ACCAA00-0439-3000-E001-00001001FFFF") 57 | ] 58 | 59 | public static var values: [XYServiceCharacteristic] = [ 60 | virtualBeaconSettings, tone, registration, inactiveVirtualBeaconSettings, inactiveInterval, gpsInterval, gpsMode, simId 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/xy3/ExtendedControlService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtendedControlService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/23/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum ExtendedControlService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Extended Control" } 14 | public var serviceUuid: CBUUID { return ExtendedControlService.serviceUuid } 15 | 16 | case simStatus 17 | case led 18 | case selfTest 19 | 20 | private static let serviceUuid = CBUUID(string: "F014AA00-0439-3000-E001-00001001FFFF") 21 | 22 | public var characteristicUuid: CBUUID { 23 | return ExtendedControlService.uuids[self]! 24 | } 25 | 26 | public var characteristicType: XYServiceCharacteristicType { 27 | return .integer 28 | } 29 | 30 | public var displayName: String { 31 | switch self { 32 | case .simStatus: return "Sim Status" 33 | case .led: return "LED" 34 | case .selfTest: return "Self Test" 35 | } 36 | } 37 | 38 | private static let uuids: [ExtendedControlService: CBUUID] = [ 39 | simStatus : CBUUID(string: "2ADDAA00-0439-3000-E001-00001001FFFF"), 40 | led : CBUUID(string: "2AAAAA00-0439-3000-E001-00001001FFFF"), 41 | selfTest : CBUUID(string: "2A77AA00-0439-3000-E001-00001001FFFF"), 42 | 43 | ] 44 | 45 | public static var values: [XYServiceCharacteristic] = [ 46 | simStatus, led, selfTest 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Gatt/xy4/XYFinderPrimaryService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrimaryService.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public enum XYFinderPrimaryService: String, XYServiceCharacteristic { 12 | 13 | public var serviceDisplayName: String { return "Primary" } 14 | public var serviceUuid: CBUUID { return XYFinderPrimaryService.serviceUuid } 15 | 16 | case stayAwake 17 | case unlock 18 | case lock 19 | case major 20 | case minor 21 | case uuid 22 | case buttonState 23 | case buzzer 24 | case buzzerConfig 25 | case adConfig 26 | case buttonConfig 27 | case lastError 28 | case uptime 29 | case reset 30 | case selfTest 31 | case debug 32 | case leftBehind 33 | case eddystoneUID 34 | case eddystoneURL 35 | case eddystoneEID 36 | case color 37 | case hardwareCreateDate 38 | 39 | private static let serviceUuid = CBUUID(string: "a44eacf4-0104-0001-0000-5f784c9977b5") 40 | 41 | public var characteristicUuid: CBUUID { 42 | return XYFinderPrimaryService.uuids[self]! 43 | } 44 | 45 | public var characteristicType: XYServiceCharacteristicType { 46 | switch self { 47 | case .major, .minor, .buttonState, .buzzer, .lastError, .uptime, .reset, .selfTest, .debug, .leftBehind: 48 | return .integer 49 | case .stayAwake, .unlock, .lock, .uuid, .buzzerConfig, .adConfig, .buttonConfig, .eddystoneUID, .eddystoneURL, .eddystoneEID, .color, .hardwareCreateDate: 50 | return .byte 51 | } 52 | } 53 | 54 | public var displayName: String { 55 | switch self { 56 | case .stayAwake: return "Stay Awake" 57 | case .unlock: return "Unlock" 58 | case .lock: return "Lock" 59 | case .major: return "Major" 60 | case .minor: return "Minor" 61 | case .uuid: return "UUID" 62 | case .buttonState: return "Button State" 63 | case .buzzer: return "Buzzer" 64 | case .buzzerConfig: return "Buzzer Config" 65 | case .adConfig: return "Ad Config" 66 | case .buttonConfig: return "Button Config" 67 | case .lastError: return "Last Error" 68 | case .uptime: return "Uptime" 69 | case .reset: return "Reset" 70 | case .selfTest: return "Self Test" 71 | case .debug: return "Debug" 72 | case .leftBehind: return "Left Behind" 73 | case .eddystoneUID: return "Eddystone UID" 74 | case .eddystoneURL: return "Eddystone URL" 75 | case .eddystoneEID: return "Eddystone EID" 76 | case .color: return "Color" 77 | case .hardwareCreateDate: return "Hardware Create Date" 78 | } 79 | } 80 | 81 | private static let uuids: [XYFinderPrimaryService: CBUUID] = [ 82 | .stayAwake : CBUUID(string: "a44eacf4-0104-0001-0001-5f784c9977b5"), 83 | .unlock : CBUUID(string: "a44eacf4-0104-0001-0002-5f784c9977b5"), 84 | .lock : CBUUID(string: "a44eacf4-0104-0001-0003-5f784c9977b5"), 85 | .major : CBUUID(string: "a44eacf4-0104-0001-0004-5f784c9977b5"), 86 | .minor : CBUUID(string: "a44eacf4-0104-0001-0005-5f784c9977b5"), 87 | .uuid : CBUUID(string: "a44eacf4-0104-0001-0006-5f784c9977b5"), 88 | .buttonState : CBUUID(string: "a44eacf4-0104-0001-0007-5f784c9977b5"), 89 | .buzzer : CBUUID(string: "a44eacf4-0104-0001-0008-5f784c9977b5"), 90 | .buzzerConfig : CBUUID(string: "a44eacf4-0104-0001-0009-5f784c9977b5"), 91 | .adConfig : CBUUID(string: "a44eacf4-0104-0001-000a-5f784c9977b5"), 92 | .buttonConfig : CBUUID(string: "a44eacf4-0104-0001-000b-5f784c9977b5"), 93 | .lastError : CBUUID(string: "a44eacf4-0104-0001-000c-5f784c9977b5"), 94 | .uptime : CBUUID(string: "a44eacf4-0104-0001-000d-5f784c9977b5"), 95 | .reset : CBUUID(string: "a44eacf4-0104-0001-000e-5f784c9977b5"), 96 | .selfTest : CBUUID(string: "a44eacf4-0104-0001-000f-5f784c9977b5"), 97 | .debug : CBUUID(string: "a44eacf4-0104-0001-0010-5f784c9977b5"), 98 | .leftBehind : CBUUID(string: "a44eacf4-0104-0001-0011-5f784c9977b5"), 99 | .eddystoneUID : CBUUID(string: "a44eacf4-0104-0001-0012-5f784c9977b5"), 100 | .eddystoneURL : CBUUID(string: "a44eacf4-0104-0001-0013-5f784c9977b5"), 101 | .eddystoneEID : CBUUID(string: "a44eacf4-0104-0001-0014-5f784c9977b5"), 102 | .color : CBUUID(string: "a44eacf4-0104-0001-0015-5f784c9977b5"), 103 | .hardwareCreateDate : CBUUID(string: "a44eacf4-0104-0001-0017-5f784c9977b5") 104 | ] 105 | 106 | public static var values: [XYServiceCharacteristic] = [ 107 | stayAwake, unlock, lock, major, minor, uuid, buttonState, buzzer, buzzerConfig, adConfig, buttonConfig, lastError, 108 | uptime, reset, selfTest, debug, leftBehind, eddystoneUID, eddystoneURL, eddystoneEID, color, hardwareCreateDate 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/Models/XYLocationCoordinate2D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYLocationCoordinate2D.swift 3 | // XYBleSdk 4 | // 5 | // Created by Arie Trouw on 4/20/17. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | public class XYLocationCoordinate2D { 12 | public var latitude : Double = 0 13 | public var longitude : Double = 0 14 | public var horizontalAccuracy : Double = 0 15 | public var verticalAccuracy : Double = 0 16 | 17 | public init() {} 18 | 19 | public init(_ location: CLLocation) { 20 | self.longitude = location.coordinate.longitude 21 | self.latitude = location.coordinate.latitude 22 | self.horizontalAccuracy = location.horizontalAccuracy 23 | self.verticalAccuracy = location.verticalAccuracy 24 | } 25 | 26 | public var isValid: Bool { 27 | return self.latitude != 0 && self.longitude != 0 28 | } 29 | 30 | var toCoreLocation: CLLocation { 31 | return CLLocation(latitude: self.latitude, longitude: self.longitude) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/XYCentral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYCentral.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/6/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreBluetooth 11 | 12 | // A wrapper around CBPeripheral, used also to mark any devices for restore or delete if the app is killed in the background 13 | public struct XYPeripheral: Hashable, Equatable { 14 | public let 15 | peripheral: CBPeripheral, 16 | advertisementData: [String: Any]?, 17 | rssi: Int 18 | 19 | let markedForDisconnect: Bool 20 | 21 | public init(_ peripheral: CBPeripheral, advertisementData: [String: Any]? = nil, rssi: Int? = nil, markedForDisconnect: Bool = false) { 22 | self.peripheral = peripheral 23 | self.advertisementData = advertisementData 24 | self.rssi = rssi ?? XYDeviceProximity.defaultProximity 25 | self.markedForDisconnect = markedForDisconnect 26 | } 27 | 28 | public static func == (lhs: XYPeripheral, rhs: XYPeripheral) -> Bool { 29 | return lhs.peripheral == rhs.peripheral 30 | } 31 | 32 | public var hashValue: Int { 33 | return self.peripheral.hashValue 34 | } 35 | 36 | public func hash(into: inout Hasher) { 37 | return self.peripheral.hash(into: &into) 38 | } 39 | } 40 | 41 | // MARK: Convert peripheral into beacon definition from ad data, used on Mac OS 42 | internal extension XYPeripheral { 43 | 44 | private func iBeaconFromUUID(_ uuid: CBUUID, family: UUID) -> XYIBeaconDefinition? { 45 | 46 | var minor = [UInt8](repeating: 0, count: 2) 47 | uuid.data.copyBytes(to: &minor, from: 0..<2) 48 | let rawMinor = Data(minor) 49 | let foundMinor = UInt16(littleEndian: rawMinor.withUnsafeBytes { $0.load(as: UInt16.self) }) & 0xfff0 50 | 51 | var major = [UInt8](repeating: 0, count: 2) 52 | uuid.data.copyBytes(to: &major, from: 2..<4) 53 | let rawMajor = Data(major) 54 | let foundMajor = UInt16(littleEndian: rawMajor.withUnsafeBytes { $0.load(as: UInt16.self) }) 55 | 56 | return XYIBeaconDefinition(uuid: family, major: foundMajor, minor: foundMinor) 57 | } 58 | 59 | var beaconDefinitionFromAdData: XYIBeaconDefinition? { 60 | if let manufacturerData = self.advertisementData?[CBAdvertisementDataManufacturerDataKey] as? Data { 61 | 62 | guard manufacturerData.count >= 25 63 | else {return nil } 64 | 65 | var companyIdentifier: UInt8 = 0 66 | manufacturerData.copyBytes(to: &companyIdentifier, from: 0..<2) 67 | guard companyIdentifier == 0x4C else { return nil } 68 | 69 | var dataType: UInt8 = 0 70 | manufacturerData.copyBytes(to: &dataType, from: 2..<3) 71 | guard dataType == 0x02 else { return nil } 72 | 73 | var dataLength: UInt8 = 0 74 | manufacturerData.copyBytes(to: &dataLength, from: 3..<4) 75 | guard dataLength == 0x15 else { return nil } 76 | 77 | var uuid = [UInt8](repeating: 0, count: 16) 78 | manufacturerData.copyBytes(to: &uuid, from: 4..<20) 79 | guard let foundUuid = UUID(uuidString: CBUUID(data: Data(uuid)).uuidString) else { return nil } 80 | 81 | var major = [UInt8](repeating: 0, count: 2) 82 | manufacturerData.copyBytes(to: &major, from: 20..<22) 83 | let rawMajor = Data(major) 84 | let foundMajor = UInt16(bigEndian: rawMajor.withUnsafeBytes { $0.load(as: UInt16.self) }) 85 | 86 | var minor = [UInt8](repeating: 0, count: 2) 87 | manufacturerData.copyBytes(to: &minor, from: 22..<24) 88 | let rawMinor = Data(minor) 89 | let foundMinor = UInt16(bigEndian: rawMinor.withUnsafeBytes { $0.load(as: UInt16.self) }) 90 | 91 | var measuredPower: UInt8 = 0 92 | manufacturerData.copyBytes(to: &measuredPower, from: 24..<25) 93 | 94 | return XYIBeaconDefinition(uuid: foundUuid, major: foundMajor, minor: foundMinor) 95 | /*} else { 96 | if let serviceids = self.advertisementData?[CBAdvertisementDataServiceUUIDsKey] as? [Any] { 97 | if let uuid = serviceids[0] as? CBUUID { 98 | if (uuid.uuidString.hasSuffix("-785F-0000-0000-0401F4AC4EA4")) { 99 | let ib = iBeaconFromUUID(uuid, family:UUID(uuidString: "a44eacf4-0104-0000-0000-5f784c9977b5")!) 100 | print ("XY4: \(ib?.major ?? 0), \(ib?.minor ?? 0)") 101 | return nil //ib 102 | } 103 | if (uuid.uuidString.hasSuffix("-DF36-484E-BC98-2D5398C5593E")) { 104 | let ib = iBeaconFromUUID(uuid, family:UUID(uuidString: "d684352e-df36-484e-bc98-2d5398c5593e")!) 105 | print ("SenX: \(ib?.major ?? 0), \(ib?.minor ?? 0)") 106 | return nil //ib 107 | } 108 | print(uuid.uuidString) 109 | return nil 110 | } 111 | return nil 112 | }*/ 113 | } else { 114 | return nil 115 | } 116 | } 117 | 118 | } 119 | 120 | public extension CBManagerState { 121 | 122 | var toString: String { 123 | switch self { 124 | case .poweredOff: return "Powered Off" 125 | case .poweredOn: return "Powered On" 126 | case .resetting: return "Resetting" 127 | case .unauthorized: return "Unauthorized" 128 | case .unknown: return "Unknown" 129 | case .unsupported: return "Unsupported" 130 | default: return "Unknown" 131 | } 132 | } 133 | } 134 | 135 | internal protocol XYCentralDelegate: class { 136 | func located(peripheral: XYPeripheral) 137 | func connected(peripheral: XYPeripheral) 138 | func timeout() 139 | func couldNotConnect(peripheral: XYPeripheral) 140 | func disconnected(periperhal: XYPeripheral) 141 | func stateChanged(newState: CBManagerState) 142 | } 143 | 144 | // Singleton wrapper around CBCentral. 145 | internal class XYCentral: NSObject { 146 | 147 | // TODO fix leak - make dictionary store weak references to delegates 148 | fileprivate var delegates = [String: XYCentralDelegate?]() 149 | 150 | public static let instance = XYCentral() 151 | 152 | fileprivate var cbManager: CBCentralManager? 153 | 154 | fileprivate var restoredPeripherals = Set() 155 | 156 | fileprivate var stopOnNoDelegates: Bool = false 157 | 158 | // All BLE operations should be done on this queue 159 | internal static let centralQueue = DispatchQueue(label:"com.xyfindables.sdk.XYCentralWorkQueue") 160 | 161 | private override init() { 162 | super.init() 163 | } 164 | 165 | public var state: CBManagerState { 166 | return self.cbManager?.state ?? .unknown 167 | } 168 | 169 | public func enable() { 170 | guard cbManager == nil || self.state != .poweredOn else { return } 171 | 172 | XYCentral.centralQueue.sync { 173 | self.cbManager = CBCentralManager( 174 | delegate: self, 175 | queue: XYCentral.centralQueue, 176 | options: [CBCentralManagerOptionRestoreIdentifierKey: "com.xyfindables.sdk.XYLocate"]) 177 | self.restoredPeripherals.removeAll() 178 | } 179 | } 180 | 181 | public func reset() { 182 | XYCentral.centralQueue.sync { 183 | self.cbManager?.delegate = nil 184 | self.cbManager = CBCentralManager( 185 | delegate: self, 186 | queue: XYCentral.centralQueue, 187 | options: [CBCentralManagerOptionRestoreIdentifierKey: "com.xyfindables.sdk.XYLocate"]) 188 | self.restoredPeripherals.removeAll() 189 | } 190 | } 191 | 192 | // Connect to an already discovered peripheral 193 | public func connect(to device: XYBluetoothDevice, options: [String: Any]? = nil) { 194 | guard let peripheral = device.peripheral else { return } 195 | cbManager?.connect(peripheral, options: options) 196 | } 197 | 198 | // Disconnect from a peripheral 199 | public func disconnect(from device: XYBluetoothDevice) { 200 | guard let peripheral = device.peripheral else { return } 201 | cbManager?.cancelPeripheralConnection(peripheral) 202 | } 203 | 204 | // Ask for devices with the requested/all services until requested to stop() 205 | public func scan(for services: [XYServiceCharacteristic]? = nil, stopOnNoDelegates: Bool = false) { 206 | guard state == .poweredOn else { return } 207 | 208 | if self.cbManager?.isScanning == true { 209 | // Stop current scan and continue 210 | self.stopScan() 211 | } 212 | 213 | self.stopOnNoDelegates = stopOnNoDelegates 214 | print("START: Scanning for devices") 215 | self.cbManager?.scanForPeripherals( 216 | withServices: services?.map { 217 | return $0.serviceUuid 218 | }, 219 | options:[CBCentralManagerScanOptionAllowDuplicatesKey: false, CBCentralManagerOptionShowPowerAlertKey: true]) 220 | } 221 | 222 | // Cancel a scan request from scan() above 223 | public func stopScan() { 224 | if stopOnNoDelegates && delegates.count > 0 { return } 225 | print("STOP: Scanning for devices") 226 | self.cbManager?.stopScan() 227 | self.stopOnNoDelegates = false 228 | } 229 | 230 | public func setDelegate(_ delegate: XYCentralDelegate, key: String) { 231 | self.delegates[key] = delegate 232 | } 233 | 234 | public func removeDelegate(for key: String) { 235 | self.delegates.removeValue(forKey: key) 236 | } 237 | } 238 | 239 | extension XYCentral: CBCentralManagerDelegate { 240 | 241 | public func centralManagerDidUpdateState(_ central: CBCentralManager) { 242 | self.delegates.forEach { 243 | $1?.stateChanged(newState: central.state) 244 | } 245 | 246 | guard central.state == .poweredOn else { return } 247 | 248 | // Disconnected any previously connected peripherals 249 | self.restoredPeripherals.filter { $0.markedForDisconnect }.forEach { 250 | self.cbManager?.cancelPeripheralConnection($0.peripheral) 251 | } 252 | } 253 | 254 | // Central delegate method called when scanForPeripherals() locates a device. The peripheral will be cached if it is not already and 255 | // the associated located() delegate method is called 256 | public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 257 | let wrappedPeripheral = XYPeripheral(peripheral, advertisementData: advertisementData, rssi: RSSI.intValue) 258 | self.delegates.forEach { $1?.located(peripheral: wrappedPeripheral) } 259 | } 260 | 261 | public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 262 | self.delegates.forEach { $1?.connected(peripheral: XYPeripheral(peripheral)) } 263 | } 264 | 265 | public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 266 | self.delegates.forEach { $1?.couldNotConnect(peripheral: XYPeripheral(peripheral)) } 267 | } 268 | 269 | public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { 270 | guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { return } 271 | 272 | // Mark any peripherals still connected from the application being closed to be deleted 273 | peripherals.forEach { peripheral in 274 | self.restoredPeripherals.insert(XYPeripheral(peripheral, markedForDisconnect: true)) 275 | } 276 | } 277 | 278 | // If the periperhal disconnects, we will reset the RSSI and report 279 | public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 280 | if let device = XYBluetoothDeviceFactory.build(from: peripheral) as? XYFinderDevice { 281 | print(" ******* OH NO: Disconnect for \(device.id.shortId) - error: \(error?.localizedDescription ?? "")") 282 | 283 | XYFinderDeviceEventManager.report(events: [.disconnected(device: device)]) 284 | guard device.markedForDeletion == false else { return } 285 | 286 | // TODO: Make sure you yank the peripheral! (Maybe...) 287 | 288 | device.resetRssi() 289 | self.delegates.forEach { $1?.disconnected(periperhal: XYPeripheral(peripheral)) } 290 | 291 | // Report exited if in background mode 292 | if XYSmartScan.instance.mode == .background { 293 | device.startMonitorTimer() 294 | } 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/XYDeviceCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYDeviceCache.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/2/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | // Holds a cache for devices that have been found via the XYLocation manager 12 | class XYDeviceCache { 13 | 14 | internal private(set) var devices = [String: XYBluetoothDevice]() 15 | private let accessQueue = DispatchQueue(label:"com.xyfindables.sdk.XYDeviceCacheQueue", attributes: .concurrent) 16 | 17 | func removeAll() { 18 | self.accessQueue.async(flags: .barrier) { 19 | self.devices.removeAll() 20 | } 21 | } 22 | 23 | func remove(at index: String) { 24 | self.accessQueue.async(flags: .barrier) { 25 | self.devices.removeValue(forKey: index) 26 | } 27 | } 28 | 29 | var count: Int { 30 | var count = 0 31 | self.accessQueue.sync { count = self.devices.count } 32 | return count 33 | } 34 | 35 | subscript(index: String) -> XYBluetoothDevice? { 36 | set { 37 | self.accessQueue.async(flags: .barrier) { 38 | self.devices[index] = newValue 39 | } 40 | } 41 | get { 42 | var device: XYBluetoothDevice? 43 | self.accessQueue.sync { 44 | device = self.devices[index] 45 | } 46 | return device 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/XYDeviceConnectionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYFinderDeviceManager.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/25/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | public final class XYDeviceConnectionManager { 12 | 13 | public static let instance = XYDeviceConnectionManager() 14 | private init() {} 15 | 16 | fileprivate var devices = [String: XYBluetoothDevice]() 17 | fileprivate var waitingDeviceIds = [String]() 18 | fileprivate let managerQueue = DispatchQueue(label:"com.xyfindables.sdk.XYFinderDeviceManagerQueue", attributes: .concurrent) 19 | fileprivate let waitQueue = DispatchQueue(label: "com.xyfindables.sdk.XYDeviceConnectionManager.WaitQueue") 20 | 21 | fileprivate let 22 | reconnectLock = GenericLock(0) 23 | 24 | fileprivate lazy var disconnectSubKeys = [String: UUID]() 25 | 26 | public var connectedDevices: [XYBluetoothDevice] { 27 | return self.devices.map { $1 } 28 | } 29 | 30 | func invalidate() { 31 | devices.forEach { $0.value.disconnect() } 32 | } 33 | 34 | // Add a tracked device and connect to it, ensuring we do not add the same device twice as this method 35 | // will be called multiple times over the course of a session from the location and peripheral delegates 36 | public func add(device: XYBluetoothDevice) { 37 | // Quick escape if we already have the device and it is connected or it's already connecting 38 | guard !isConnectedOrConnecting(for: self.devices[device.id]) else { return } 39 | 40 | // Check and connect 41 | guard self.devices[device.id] == nil else { return } 42 | self.devices[device.id] = device 43 | self.connect(to: device) 44 | } 45 | 46 | // Remove the devices from the dictionary of tracked, connected devices, and let central know to disconnect 47 | internal func remove(for id: String, disconnect: Bool) { 48 | guard let device = self.devices[id] else { return } 49 | self.devices.removeValue(forKey: device.id) 50 | self.waitingDeviceIds.removeAll(where: { $0 == device.id }) 51 | if disconnect && (device.state != .disconnected || device.state != .disconnecting) { 52 | self.disconnect(from: device) 53 | } 54 | } 55 | 56 | // Like above, but this is a hard reset for the device, removing the peripheral so it can be rediscovered 57 | // Used by the firmware update 58 | internal func remove(device: XYBluetoothDevice) { 59 | guard self.devices[device.id] != nil else { return } 60 | self.devices.removeValue(forKey: device.id) 61 | self.waitingDeviceIds.removeAll(where: { $0 == device.id }) 62 | if device.state != .disconnected || device.state != .disconnecting { 63 | XYCentral.instance.disconnect(from: device) 64 | device.detachPeripheral() 65 | } 66 | } 67 | 68 | // If we lose connection to a device, we can put it in the wait queue and it will automatically reconnect 69 | // even if the user leaves the area (as long as the app is still running in the backgound) 70 | func wait(for device: XYBluetoothDevice) { 71 | // Quick escape if we already have the device and it is connected or it's already connecting 72 | guard !isConnectedOrConnecting(for: device) else { return } 73 | 74 | // We have lost contact with the device, so we'll do a non-expiring connectiong try 75 | guard !waitingDeviceIds.contains(where: { $0 == device.id }) else { return } 76 | self.managerQueue.async(flags: .barrier) { 77 | guard !self.waitingDeviceIds.contains(where: { $0 == device.id }) else { return } 78 | print("Adding \(device.id) to wait queue...") 79 | 80 | self.waitingDeviceIds.append(device.id) 81 | 82 | XYConnectionAgent(for: device).connect(.never).then(on: self.waitQueue) { 83 | guard let xyDevice = device as? XYFinderDevice else { 84 | self.reconnectLock.unlock() 85 | return 86 | } 87 | 88 | // Check to see if we still want to connect to this 89 | guard self.waitingDeviceIds.contains(xyDevice.id) else { 90 | xyDevice.disconnect() 91 | self.reconnectLock.unlock() 92 | return 93 | } 94 | 95 | self.waitingDeviceIds.removeAll(where: { $0 == device.id }) 96 | print("\(device.id) is found again!") 97 | 98 | // Lock and try for a reconnection 99 | xyDevice.connection { 100 | // If we have an XY Finder device, we report this, subscribe to the button and kick off the RSSI read loop 101 | if let xyDevice = device as? XYFinderDevice { 102 | if xyDevice.unlock().hasError { 103 | throw XYBluetoothError.couldNotConnect 104 | } 105 | 106 | if xyDevice.subscribeToButtonPress().hasError { 107 | throw XYBluetoothError.couldNotConnect 108 | } 109 | 110 | xyDevice.peripheral?.readRSSI() 111 | } 112 | 113 | }.then(on: self.waitQueue) { 114 | if let xyDevice = device as? XYFinderDevice { 115 | XYFinderDeviceEventManager.report(events: [.reconnected(device: xyDevice)]) 116 | } 117 | self.reconnectLock.unlock() 118 | 119 | }.always(on: self.waitQueue) { 120 | self.reconnectLock.unlock() 121 | } 122 | } 123 | 124 | self.reconnectLock.lock() 125 | } 126 | } 127 | } 128 | 129 | // MARK: Connect and disconnection 130 | private extension XYDeviceConnectionManager { 131 | 132 | func isConnectedOrConnecting(for device: XYBluetoothDevice?) -> Bool { 133 | if let xyDevice = device as? XYFinderDevice { 134 | if xyDevice.state == .connecting { return true } 135 | if xyDevice.state == .connected { 136 | XYFinderDeviceEventManager.report(events: [.alreadyConnected(device: xyDevice)]) 137 | return true 138 | } 139 | } 140 | 141 | return false 142 | } 143 | 144 | // Connect to the device using the connection agent, then subscribe to the button press and 145 | // start the readRSSI recursive loop. Use a 0-based sempahore to ensure only once device 146 | // can be in the connection state at one time 147 | func connect(to device: XYBluetoothDevice) { 148 | let deviceId = device.id 149 | 150 | print("STEP 1: Trying to connect to \(deviceId.shortId)...") 151 | 152 | // If we disconnect at any point in the connection, we remove the device so it can be tried again and unlock the connection semaphore 153 | // We also try to run the disconnect call in case we are watching the notifications 154 | self.disconnectSubKeys[deviceId] = XYFinderDeviceEventManager.subscribe(to: [.disconnected]) { [weak self] event in 155 | XYFinderDeviceEventManager.unsubscribe(to: [.disconnected], referenceKey: self?.disconnectSubKeys[deviceId]) 156 | guard let finder = device as? XYFinderDevice, finder == event.device else { return } 157 | self?.devices[finder.id] = nil 158 | } 159 | 160 | let connectionQueue = DispatchQueue(label: "com.xyfindables.sdk.ConnectionManagerQueueFor\(deviceId.shortId)") 161 | 162 | device.connection { 163 | // If we have an XY Finder device, we report this, subscribe to the button and kick off the RSSI read loop 164 | if let xyDevice = device as? XYFinderDevice { 165 | if xyDevice.unlock().hasError { 166 | throw XYBluetoothError.couldNotConnect 167 | } 168 | 169 | if xyDevice.subscribeToButtonPress().hasError { 170 | throw XYBluetoothError.couldNotConnect 171 | } 172 | 173 | xyDevice.peripheral?.readRSSI() 174 | } 175 | 176 | }.then(on: connectionQueue) { 177 | if let xyDevice = device as? XYFinderDevice { 178 | XYFinderDeviceEventManager.report(events: [.connected(device: xyDevice)]) 179 | } 180 | 181 | if self.waitingDeviceIds.contains(deviceId) { 182 | self.waitingDeviceIds.removeAll(where: { $0 == deviceId }) 183 | } 184 | 185 | }.catch(on: connectionQueue) { error in 186 | guard let xyError = error as? XYBluetoothError, let xyDevice = device as? XYFinderDevice else { return } 187 | switch xyError { 188 | case .timedOut: 189 | XYFinderDeviceEventManager.report(events: [.timedOut(device: xyDevice, type: .connection)]) 190 | default: 191 | XYFinderDeviceEventManager.report(events: [.connectionError(device: xyDevice, error: xyError)]) 192 | } 193 | 194 | print("STEP 6: ERROR for \((error as! XYBluetoothError).toString) for device \(deviceId)") 195 | 196 | // Completely disconnect so we can retry if there is any connection issue 197 | XYCentral.instance.disconnect(from: device) 198 | XYBluetoothDeviceFactory.remove(device: xyDevice) 199 | self.devices.removeValue(forKey: deviceId) 200 | self.waitingDeviceIds.removeAll(where: { $0 == deviceId }) 201 | 202 | }.always(on: connectionQueue) { 203 | XYFinderDeviceEventManager.unsubscribe(to: [.disconnected], referenceKey: self.disconnectSubKeys[deviceId]) 204 | } 205 | } 206 | 207 | func disconnect(from device: XYBluetoothDevice) { 208 | print("STEP 1: Trying to DISCONNECT from \(device.id.shortId)...") 209 | 210 | let disconnectQueue = DispatchQueue(label: "com.xyfindables.sdk.ConnectionManagerDisconnectQueueFor\(device.id)") 211 | 212 | device.connection { 213 | // If we have an XY Finder device of a particular family, we unsubscribe from the button press and disconnect 214 | if let xyDevice = device as? XYFinderDevice, ( 215 | xyDevice.family.id == XY3BluetoothDevice.id || 216 | xyDevice.family.id == XY4BluetoothDevice.id || 217 | xyDevice.family.id == XYGPSBluetoothDevice.id) { 218 | 219 | if xyDevice.unlock().hasError { 220 | throw XYBluetoothError.couldNotConnect 221 | } 222 | 223 | if xyDevice.unsubscribeToButtonPress(for: nil).hasError { 224 | throw XYBluetoothError.couldNotConnect 225 | } 226 | } 227 | 228 | }.always(on: disconnectQueue) { 229 | print("STEP 2: Always on DISCONNECT from \(device.id.shortId)") 230 | XYCentral.instance.disconnect(from: device) 231 | device.detachPeripheral() 232 | if let xyDevice = device as? XYFinderDevice { 233 | XYBluetoothDeviceFactory.remove(device: xyDevice) 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/XYGeocode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYGeocode.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 10/17/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | public class XYGeocode { 12 | 13 | private static let geocoder = CLGeocoder() 14 | 15 | public class func geocodeLocation(latitude: Float, longitude: Float, callback: ((String?) -> Void)? = nil) { 16 | let location = CLLocation(latitude: CLLocationDegrees(latitude), longitude: CLLocationDegrees(longitude)) 17 | geocoder.reverseGeocodeLocation(location) { placemarks, error in 18 | if let placemarks = placemarks, placemarks.count > 0 { 19 | callback?(placemarks[0].name) 20 | } else { 21 | callback?(nil) 22 | } 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/XYLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYLocation.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/7/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import CoreBluetooth 11 | 12 | public protocol XYLocationDelegate: class { 13 | func didRangeBeacons(_ beacons: [XYBluetoothDevice], for family: XYDeviceFamily?) 14 | func deviceEntered(_ device: XYBluetoothDevice) 15 | func deviceExited(_ device: XYBluetoothDevice) 16 | func deviceExiting(_ device: XYBluetoothDevice) 17 | func locationsUpdated(_ locations: [XYLocationCoordinate2D]) 18 | } 19 | 20 | public class XYLocation: NSObject { 21 | 22 | public static let instance = XYLocation() 23 | 24 | fileprivate let manager = CLLocationManager() 25 | 26 | fileprivate lazy var delegates = [String: XYLocationDelegate?]() 27 | 28 | private override init() { 29 | super.init() 30 | #if os(iOS) 31 | self.manager.delegate = self 32 | #endif 33 | } 34 | 35 | public func setDelegate(_ delegate: XYLocationDelegate, key: String) { 36 | self.delegates[key] = delegate 37 | } 38 | 39 | public var userLocation: CLLocationCoordinate2D? { 40 | return self.manager.location?.coordinate 41 | } 42 | } 43 | 44 | // MARK: Start and stop 45 | public extension XYLocation { 46 | 47 | func start() { 48 | self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters 49 | self.manager.distanceFilter = XYConstants.DEVICE_TUNING_LOCATION_CHANGE_THRESHOLD 50 | #if os(iOS) 51 | self.manager.allowsBackgroundLocationUpdates = true 52 | self.manager.requestAlwaysAuthorization() 53 | self.manager.startUpdatingLocation() 54 | #endif 55 | } 56 | 57 | func stop() { 58 | self.manager.stopUpdatingLocation() 59 | } 60 | } 61 | 62 | #if os(iOS) 63 | // MARK: Passthrough methods 64 | public extension XYLocation { 65 | 66 | var locationServicesEnabled: Bool { 67 | return CLLocationManager.locationServicesEnabled() 68 | } 69 | 70 | var authorizationStatus: CLAuthorizationStatus { 71 | return CLLocationManager.authorizationStatus() 72 | } 73 | 74 | } 75 | 76 | // MARK: Ranging methods (used for foreground operations) 77 | extension XYLocation { 78 | 79 | // Convenience method 80 | public func startRanging(for families: [XYDeviceFamily]) { 81 | 82 | families.forEach { startRanging(for: $0) } 83 | } 84 | 85 | // Start ranging for a particular type of XY device 86 | public func startRanging(for family: XYDeviceFamily) { 87 | guard let device = XYBluetoothDeviceFactory.build(from: family) else { return } 88 | self.startRanging(for: device) 89 | } 90 | 91 | public func startRanging(for devices: [XYBluetoothDevice]) { 92 | // Get the existing regions that location manager is looking for 93 | let rangedDevices = manager.rangedRegions 94 | .compactMap { $0 as? CLBeaconRegion } 95 | .filter { $0.minor != nil && $0.major != nil } 96 | .compactMap { XYBluetoothDeviceFactory.build(from: $0.xyiBeaconDefinition ) } 97 | 98 | // Remove devices from rangning that are not on the list 99 | rangedDevices.filter { device in !devices.contains(where: { $0.id == device.id }) }.forEach { self.stopRanging(for: $0) } 100 | 101 | // Add unranged devices 102 | devices.filter { device in !rangedDevices.contains(where: { $0.id == device.id }) }.forEach { self.startRanging(for: $0) } 103 | } 104 | 105 | public func startRanging(for device: XYBluetoothDevice) { 106 | let beaconRegion = CLBeaconRegion(proximityUUID: device.family.uuid, identifier: device.id) 107 | manager.startRangingBeacons(in: beaconRegion) 108 | } 109 | 110 | public func clearRanging() { 111 | manager.rangedRegions 112 | .compactMap { $0 as? CLBeaconRegion } 113 | .forEach { manager.stopRangingBeacons(in: $0) } 114 | } 115 | 116 | public func stopRanging(for device: XYBluetoothDevice) { 117 | manager.stopRangingBeacons(in: device.beaconRegion(device.family.uuid, slot: 4)) 118 | manager.stopRangingBeacons(in: device.beaconRegion(device.family.uuid, slot: 7)) 119 | manager.stopRangingBeacons(in: device.beaconRegion(device.family.uuid, slot: 8)) 120 | } 121 | } 122 | 123 | // MARK: Monitoring methods (used for background operation) 124 | public extension XYLocation { 125 | 126 | func clearMonitoring() { 127 | self.manager.monitoredRegions.forEach { region in 128 | self.manager.stopMonitoring(for: region) 129 | } 130 | } 131 | 132 | // Convenience method 133 | func startMonitoring(for families: [XYDeviceFamily]) { 134 | 135 | families.forEach { startMonitoring(for: $0, isHighPriority: false) } 136 | } 137 | 138 | func startMonitoring(for family: XYDeviceFamily, isHighPriority: Bool) { 139 | 140 | guard let device = XYBluetoothDeviceFactory.build(from: family) else { 141 | return 142 | } 143 | 144 | self.startMonitoring(for: device, isHighPriority: isHighPriority) 145 | } 146 | 147 | func startMonitoring(for devices: [XYBluetoothDevice]) { 148 | // Get the existing regions that location manager is looking for 149 | let monitoredDevices = manager.monitoredRegions 150 | .compactMap { $0 as? CLBeaconRegion } 151 | .filter { $0.minor != nil && $0.major != nil } 152 | .compactMap { XYBluetoothDeviceFactory.build(from: $0.xyiBeaconDefinition ) } 153 | 154 | // Remove devices from monitored that are not on the list 155 | monitoredDevices.filter { device in !devices.contains(where: { $0.id == device.id }) }.forEach { 156 | self.stopMonitoring(for: $0) 157 | } 158 | 159 | // Add unmonitored devices 160 | devices.filter { device in !monitoredDevices.contains(where: { $0.id == device.id }) }.forEach { 161 | self.startMonitoring(for: $0, isHighPriority: false) 162 | } 163 | } 164 | 165 | func startMonitoring(for device: XYBluetoothDevice, isHighPriority: Bool) { 166 | if isHighPriority { 167 | //monitor for button presses also, aka power level 8 168 | let beaconRegionLevel8 = device.beaconRegion(slot: 8) 169 | beaconRegionLevel8.notifyOnExit = false 170 | beaconRegionLevel8.notifyOnEntry = false 171 | beaconRegionLevel8.notifyEntryStateOnDisplay = false 172 | self.manager.startMonitoring(for: beaconRegionLevel8) 173 | } 174 | 175 | //always monitor power level 4 176 | let beaconRegionLevel4 = device.beaconRegion(slot: 4) 177 | beaconRegionLevel4.notifyOnExit = true 178 | beaconRegionLevel4.notifyOnEntry = true 179 | beaconRegionLevel4.notifyEntryStateOnDisplay = true 180 | self.manager.startMonitoring(for: beaconRegionLevel4) 181 | } 182 | 183 | func stopMonitoring(for device: XYBluetoothDevice) { 184 | manager.stopMonitoring(for: device.beaconRegion(device.family.uuid, slot: 4)) 185 | manager.stopMonitoring(for: device.beaconRegion(device.family.uuid, slot: 7)) 186 | manager.stopMonitoring(for: device.beaconRegion(device.family.uuid, slot: 8)) 187 | } 188 | 189 | } 190 | 191 | // MARK: CLLocationManagerDelegate 192 | extension XYLocation: CLLocationManagerDelegate { 193 | 194 | // This callback drives the update cycle which ensures we are still connected to a device by testing the last ping time 195 | public func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { 196 | let processedBeacons = beacons.compactMap { XYBluetoothDeviceFactory.build(from: $0.xyiBeaconDefinition, rssi: $0.rssi, updateRssiAndPower: true) } 197 | self.delegates.forEach { 198 | $1?.didRangeBeacons(processedBeacons,for: region.family) 199 | } 200 | } 201 | 202 | public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { 203 | guard 204 | let beaconRegion = region as? CLBeaconRegion, 205 | let device = XYBluetoothDeviceFactory.build(from: beaconRegion.xyiBeaconDefinition) 206 | else { return } 207 | 208 | 209 | self.delegates.forEach { 210 | $1?.deviceEntered(device) 211 | 212 | } 213 | } 214 | 215 | public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { 216 | guard 217 | let beaconRegion = region as? CLBeaconRegion, 218 | let device = XYBluetoothDeviceFactory.build(from: beaconRegion.xyiBeaconDefinition) 219 | else { return } 220 | 221 | self.delegates.forEach { 222 | $1?.deviceExited(device) 223 | } 224 | } 225 | 226 | public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 227 | // TODO updateStatus() 228 | } 229 | 230 | public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 231 | self.delegates.forEach { 232 | $1?.locationsUpdated(locations.map { XYLocationCoordinate2D($0) }) 233 | 234 | } 235 | } 236 | 237 | public func locationManagerShouldDisplayHeadingCalibration(_ manager: CLLocationManager) -> Bool { 238 | return false 239 | } 240 | } 241 | #endif 242 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/XYSmartScan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYSmartScan.swift 3 | // XYBleSdk 4 | // 5 | // Created by Darren Sutherland on 9/10/18. 6 | // Copyright © 2018 XY - The Findables Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreBluetooth 11 | 12 | public protocol XYSmartScanDelegate { 13 | func smartScan(status: XYSmartScanStatus) 14 | func smartScan(location: XYLocationCoordinate2D) 15 | func smartScan(detected device: XYBluetoothDevice, rssi: Int, family: XYDeviceFamily) 16 | func smartScan(detected devices: [XYBluetoothDevice], family: XYDeviceFamily) 17 | func smartScan(entered device: XYBluetoothDevice) 18 | func smartScan(exiting device:XYBluetoothDevice) 19 | func smartScan(exited device: XYBluetoothDevice) 20 | } 21 | 22 | public enum XYSmartScanStatus: Int { 23 | case none 24 | case enabled 25 | case bluetoothUnavailable 26 | case bluetoothDisabled 27 | case backgroundLocationDisabled 28 | case locationDisabled 29 | } 30 | 31 | public enum XYSmartScanMode { 32 | case initializing 33 | case foreground 34 | case background 35 | } 36 | 37 | public class XYSmartScan { 38 | 39 | public static let instance = XYSmartScan() 40 | 41 | // TODO fix leak - make dictionary store weak references to delegates 42 | fileprivate var delegates = [String: XYSmartScanDelegate?]() 43 | 44 | // TODO fix leak - make dictionary store weak references to tracked devices 45 | fileprivate var trackedDevices = [String: XYFinderDevice]() 46 | 47 | fileprivate lazy var currentDiscoveryList = [XYDeviceFamily]() 48 | 49 | fileprivate let location = XYLocation.instance 50 | fileprivate let central = XYCentral.instance 51 | 52 | public fileprivate(set) var currentLocation = XYLocationCoordinate2D() 53 | public fileprivate(set) var currentStatus = XYSmartScanStatus.none 54 | fileprivate var isActive: Bool = false 55 | 56 | public fileprivate(set) var mode: XYSmartScanMode = .initializing 57 | 58 | fileprivate var isCheckingExits: Bool = false 59 | 60 | internal static let queue = DispatchQueue(label: String(format: "com.xyfindables.sdk.XYSmartScan")) 61 | 62 | private init() { 63 | #if os(iOS) 64 | self.location.setDelegate(self, key: "XYSmartScan") 65 | self.central.setDelegate(self, key: "XYSmartScan") 66 | #elseif os(macOS) 67 | self.central.setDelegate(self, key: "XYSmartScan") 68 | #endif 69 | } 70 | 71 | public func start(for families: [XYDeviceFamily] = XYDeviceFamily.allFamlies(), mode: XYSmartScanMode) { 72 | if mode == self.mode { return } 73 | 74 | // For iOS, we use the Location manager to range/monitor for iBeacon devices 75 | #if os(iOS) 76 | self.location.start() 77 | 78 | switch mode { 79 | case .foreground: self.switchToForeground(families) 80 | case .background: self.switchToBackground(families) 81 | default: self.switchToForeground(families) 82 | } 83 | 84 | self.isActive = true 85 | #endif 86 | 87 | // In the case of macOS, we use central to discover and filter devices on ad data to determine if they are iBeacons 88 | #if os(macOS) 89 | self.currentDiscoveryList = families 90 | 91 | self.central.state == .poweredOn ? 92 | self.central.scan() : 93 | self.central.enable() 94 | 95 | self.isActive = true 96 | #endif 97 | } 98 | 99 | public func stop() { 100 | guard isActive else { return } 101 | 102 | #if os(iOS) 103 | self.location.stop() 104 | self.location.clearMonitoring() 105 | self.location.clearRanging() 106 | #endif 107 | 108 | #if os(macOS) 109 | self.central.stopScan() 110 | self.currentDiscoveryList.removeAll() 111 | #endif 112 | 113 | self.trackedDevices.removeAll() 114 | 115 | self.isActive = false 116 | self.isCheckingExits = false 117 | self.mode = .background 118 | } 119 | 120 | public func setDelegate(_ delegate: XYSmartScanDelegate, key: String) { 121 | self.delegates[key] = delegate 122 | } 123 | 124 | public func removeDelegate(for key: String) { 125 | self.delegates.removeValue(forKey: key) 126 | } 127 | 128 | public func invalidateSession() { 129 | XYDeviceConnectionManager.instance.invalidate() 130 | XYBluetoothDeviceFactory.invalidateCache() 131 | } 132 | 133 | public var trackDevicesCount: Int { 134 | return self.trackedDevices.count 135 | } 136 | } 137 | 138 | // MARK: Change monitoring state based on start/stop 139 | fileprivate extension XYSmartScan { 140 | 141 | func switchToForeground(_ families: [XYDeviceFamily]) { 142 | guard self.mode != .foreground else { return } 143 | 144 | self.mode = .foreground 145 | 146 | #if os(iOS) 147 | self.location.clearMonitoring() 148 | self.location.startRanging(for: families) 149 | #endif 150 | 151 | self.central.state == .poweredOn ? 152 | self.central.scan() : 153 | self.central.enable() 154 | 155 | self.isCheckingExits = true 156 | self.checkExits() 157 | self.updateTracking() 158 | self.updateStatus() 159 | } 160 | 161 | func switchToBackground(_ families: [XYDeviceFamily]) { 162 | guard self.mode != .background else { return } 163 | 164 | self.mode = .background 165 | #if os(iOS) 166 | self.location.clearRanging() 167 | #endif 168 | self.isCheckingExits = false 169 | #if os(iOS) 170 | self.location.startMonitoring(for: families) 171 | #endif 172 | self.updateTracking() 173 | self.updateStatus() 174 | } 175 | 176 | } 177 | 178 | // MARK: Status updates 179 | extension XYSmartScan { 180 | 181 | public func updateStatus() { 182 | #if os(iOS) 183 | var newStatus = XYSmartScanStatus.enabled 184 | let central = XYCentral.instance 185 | if !XYLocation.instance.locationServicesEnabled { 186 | newStatus = .locationDisabled 187 | } else { 188 | let authorizationStatus = XYLocation.instance.authorizationStatus 189 | if authorizationStatus != .authorizedAlways && authorizationStatus != .notDetermined { 190 | newStatus = .backgroundLocationDisabled 191 | } 192 | } 193 | 194 | switch central.state { 195 | case .unknown: 196 | newStatus = .none 197 | case .poweredOn: 198 | newStatus = .enabled 199 | break 200 | case .poweredOff: 201 | newStatus = .bluetoothDisabled 202 | break 203 | case .unsupported: 204 | newStatus = .bluetoothUnavailable 205 | break 206 | case .unauthorized: 207 | newStatus = .backgroundLocationDisabled 208 | break 209 | case .resetting: 210 | newStatus = .none 211 | break 212 | default: 213 | break; 214 | } 215 | 216 | if self.currentStatus != newStatus { 217 | self.currentStatus = newStatus 218 | // Currently used only by the app for displaying BLE/Location status 219 | self.delegates.map { $1 }.forEach { $0?.smartScan(status: self.currentStatus)} 220 | } 221 | #endif 222 | } 223 | 224 | } 225 | 226 | // MARK: Tracking wranglers for known devices 227 | public extension XYSmartScan { 228 | 229 | // Called from the application code, used to track a device that is assigne to the user 230 | func startTracking(for device: XYFinderDevice) { 231 | XYSmartScan.queue.sync { 232 | guard trackedDevices[device.id] == nil else { return } 233 | trackedDevices[device.id] = device 234 | updateTracking() 235 | } 236 | } 237 | 238 | func stopTracking(for deviceId: String) { 239 | XYSmartScan.queue.sync { 240 | guard trackedDevices[deviceId] != nil else { return } 241 | trackedDevices.removeValue(forKey: deviceId) 242 | updateTracking() 243 | } 244 | } 245 | 246 | fileprivate func updateTracking() { 247 | #if os(iOS) 248 | self.mode == .foreground ? 249 | location.startRanging(for: self.trackedDevices.map { $1 as XYBluetoothDevice } ) : 250 | location.startMonitoring(for: self.trackedDevices.map { $1 } ) 251 | #endif 252 | } 253 | 254 | // Another recursive method for checking exits of devices so we can alter the user 255 | fileprivate func checkExits() { 256 | XYSmartScan.queue.asyncAfter(deadline: DispatchTime.now() + TimeInterval(XYConstants.DEVICE_TUNING_SECONDS_EXIT_CHECK_INTERVAL)) { 257 | guard self.isCheckingExits else { 258 | return 259 | } 260 | 261 | // Loop through known devices that are connected 262 | let connectedDevices = self.trackedDevices.values // XYDeviceConnectionManager.instance.connectedDevices 263 | for device in connectedDevices { 264 | guard 265 | let xyDevice = device as? XYFinderDevice, 266 | let lastPulseTime = device.lastPulseTime, 267 | device.isUpdatingFirmware == false, 268 | fabs(lastPulseTime.timeIntervalSinceNow) > XYConstants.DEVICE_TUNING_SECONDS_WITHOUT_SIGNAL_FOR_EXITING 269 | else { continue } 270 | 271 | // Currently used by the refresh signal meters, this will show .none 272 | XYFinderDeviceEventManager.report(events: [.exiting(device: xyDevice)]) 273 | xyDevice.verifyExit(nil) 274 | } 275 | 276 | self.checkExits() 277 | } 278 | } 279 | } 280 | 281 | #if os(iOS) 282 | // MARK: BLELocationDelegate - Location monitoring and ranging delegates 283 | extension XYSmartScan: XYLocationDelegate { 284 | public func deviceExiting(_ device: XYBluetoothDevice) { 285 | self.delegates.forEach { $1?.smartScan(exiting: device) } 286 | } 287 | 288 | public func locationsUpdated(_ locations: [XYLocationCoordinate2D]) { 289 | locations.forEach { location in 290 | self.delegates.forEach { $1?.smartScan(location: location) } 291 | XYBluetoothDeviceFactory.updateDeviceLocations(location) 292 | } 293 | } 294 | 295 | public func didRangeBeacons(_ beacons: [XYBluetoothDevice], for family: XYDeviceFamily?) { 296 | guard let family = family else { return } 297 | 298 | let uniqueBeacons = beacons.reduce([], { initial, beacon in 299 | initial.contains(where: { $0.id == beacon.id }) ? initial : initial + [beacon] 300 | }) 301 | 302 | uniqueBeacons.forEach { beacon in 303 | if !beacon.inRange { 304 | self.delegates.forEach { $1?.smartScan(entered: beacon)} 305 | XYFinderDeviceEventManager.report(events: [.entered(device: beacon)]) 306 | } 307 | 308 | self.delegates.forEach { 309 | $1?.smartScan(detected: beacon, rssi: beacon.rssi, family: family) 310 | } 311 | 312 | // Handles button presses and other notifications 313 | beacon.detected(beacon.rssi) 314 | } 315 | 316 | self.delegates.forEach { 317 | $1?.smartScan(detected: uniqueBeacons, family: family) 318 | } 319 | } 320 | 321 | public func deviceEntered(_ device: XYBluetoothDevice) { 322 | self.delegates.forEach { $1?.smartScan(entered: device) } 323 | print("MONITOR ENTER: Device \(device.id)") 324 | (device as? XYFinderDevice)?.cancelMonitorTimer() 325 | } 326 | 327 | public func deviceExited(_ device: XYBluetoothDevice) { 328 | self.delegates.forEach { $1?.smartScan(exited: device) } 329 | print("MONITOR EXIT: Device \(device.id)") 330 | (device as? XYFinderDevice)?.startMonitorTimer() 331 | } 332 | 333 | } 334 | #endif 335 | 336 | //#if os(macOS) 337 | extension XYSmartScan: XYCentralDelegate { 338 | public func stateChanged(newState: CBManagerState) { 339 | if newState == .poweredOn { 340 | self.central.scan() 341 | } 342 | updateStatus() 343 | } 344 | 345 | public func located(peripheral: XYPeripheral) { 346 | guard 347 | let beacon = peripheral.beaconDefinitionFromAdData, 348 | let device = XYBluetoothDeviceFactory.build(from: beacon, rssi: peripheral.rssi, updateRssiAndPower: true) 349 | else { return } 350 | 351 | let family = device.family 352 | 353 | if !device.inRange { 354 | self.delegates.forEach { $1?.smartScan(entered: device)} 355 | XYFinderDeviceEventManager.report(events: [.entered(device: device)]) 356 | } 357 | 358 | self.delegates.forEach { 359 | $1?.smartScan(detected: device, rssi: device.rssi, family: family) 360 | } 361 | 362 | // Handles button presses and other notifications 363 | device.detected(peripheral.rssi) 364 | 365 | self.delegates.forEach { $1?.smartScan(detected: [device], family: family) } 366 | } 367 | 368 | public func connected(peripheral: XYPeripheral) {} 369 | public func timeout() {} 370 | public func couldNotConnect(peripheral: XYPeripheral) {} 371 | public func disconnected(periperhal: XYPeripheral) {} 372 | } 373 | //#endif 374 | -------------------------------------------------------------------------------- /Sources/XyBleSdk/XyBleSdk.swift: -------------------------------------------------------------------------------- 1 | struct XyBleSdk { 2 | var text = "Hello, World!" 3 | } 4 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import XyBleSdkTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += XyBleSdkTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/XyBleSdkTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(XyBleSdkTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/XyBleSdkTests/XyBleSdkTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import XyBleSdk 3 | 4 | final class XyBleSdkTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | --------------------------------------------------------------------------------