├── .editorconfig ├── .github ├── pull_request_template.md └── workflows │ ├── swift-wasm.yml │ ├── swift-windows.yml │ ├── swift.yml │ └── swift-arm.yml ├── Sources ├── DarwinGATT │ ├── Extensions │ │ ├── CBCentral.swift │ │ ├── Integer.swift │ │ └── CBPeripheral.swift │ ├── DarwinCentralError.swift │ ├── DarwinBluetoothState.swift │ ├── Queue.swift │ ├── DarwinAttributes.swift │ ├── PeripheralContinuation.swift │ ├── DarwinAdvertisementData.swift │ ├── DarwinDescriptor.swift │ └── DarwinPeripheral.swift └── GATT │ ├── L2CAP.swift │ ├── Extensions │ └── OptionSet.swift │ ├── GATT.docc │ └── GATT.md │ ├── Peer.swift │ ├── ManufacturerSpecificData.swift │ ├── ScanData.swift │ ├── MaximumTransmissionUnit.swift │ ├── GATTServerConnection.swift │ ├── CentralAttributes.swift │ ├── CharacteristicProperty.swift │ ├── AsyncStream.swift │ ├── CentralError.swift │ ├── AttributePermission.swift │ ├── CentralProtocol.swift │ ├── PeripheralProtocol.swift │ ├── AdvertisementData.swift │ ├── GATTCentral.swift │ ├── GATTClientConnection.swift │ └── GATTPeripheral.swift ├── LICENSE ├── .devcontainer └── devcontainer.json ├── .gitignore ├── Package.swift ├── README.md └── Tests └── GATTTests ├── TestHostController.swift ├── TestL2CAPSocket.swift └── GATTTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Issue** 2 | 3 | Fixes #1. 4 | 5 | **What does this PR Do?** 6 | 7 | Description of the changes in this pull request. 8 | 9 | **Where should the reviewer start?** 10 | 11 | `main.swift` 12 | 13 | **Sweet giphy showing how you feel about this PR** 14 | 15 | ![Giphy](https://media.giphy.com/media/rkDXJA9GoWR2/giphy.gif) 16 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/Extensions/CBCentral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CBCentral.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 22/12/21. 6 | // 7 | 8 | #if canImport(CoreBluetooth) 9 | import Foundation 10 | import CoreBluetooth 11 | 12 | internal extension CBCentral { 13 | 14 | var id: UUID { 15 | if #available(macOS 10.13, *) { 16 | return (self as CBPeer).identifier 17 | } else { 18 | return self.value(forKey: "identifier") as! UUID 19 | } 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/Extensions/Integer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Integer.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 8/24/15. 6 | // Copyright © 2015 PureSwift. All rights reserved. 7 | // 8 | 9 | internal extension UInt16 { 10 | 11 | /// Initializes value from two bytes. 12 | init(bytes: (UInt8, UInt8)) { 13 | self = unsafeBitCast(bytes, to: UInt16.self) 14 | } 15 | 16 | /// Converts to two bytes. 17 | var bytes: (UInt8, UInt8) { 18 | return unsafeBitCast(self, to: (UInt8, UInt8).self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/swift-wasm.yml: -------------------------------------------------------------------------------- 1 | name: Swift WASM 2 | on: [push] 3 | jobs: 4 | build-wasm: 5 | name: Build for WASM 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | swift: ["6.0.3"] 10 | config: ["debug", "release"] 11 | container: swift:${{ matrix.swift }} 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Swift Version 16 | run: swift --version 17 | - uses: swiftwasm/setup-swiftwasm@v2 18 | - name: Build 19 | run: swift build -c ${{ matrix.config }} --swift-sdk wasm32-unknown-wasi 20 | -------------------------------------------------------------------------------- /.github/workflows/swift-windows.yml: -------------------------------------------------------------------------------- 1 | name: Swift Windows 2 | on: [push] 3 | jobs: 4 | windows-build: 5 | name: Windows 6 | runs-on: windows-latest 7 | strategy: 8 | matrix: 9 | swift: ["6.1.2"] 10 | config: ["debug", "release"] 11 | steps: 12 | - uses: compnerd/gha-setup-swift@main 13 | with: 14 | branch: swift-${{ matrix.swift }}-release 15 | tag: ${{ matrix.swift }}-RELEASE 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Swift Version 19 | run: swift --version 20 | - name: Build 21 | run: swift build -c ${{ matrix.config }} -------------------------------------------------------------------------------- /Sources/DarwinGATT/Extensions/CBPeripheral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CBPeripheral.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 22/12/21. 6 | // 7 | 8 | #if canImport(CoreBluetooth) 9 | import Foundation 10 | import CoreBluetooth 11 | 12 | internal extension CBPeripheral { 13 | 14 | var id: UUID { 15 | if #available(macOS 10.13, *) { 16 | return (self as CBPeer).identifier 17 | } else { 18 | return self.value(forKey: "identifier") as! UUID 19 | } 20 | } 21 | 22 | var mtuLength: NSNumber { 23 | return self.value(forKey: "mtuLength") as! NSNumber 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/GATT/L2CAP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L2CAP.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 4/18/22. 6 | // 7 | 8 | #if canImport(BluetoothHCI) 9 | import Bluetooth 10 | import BluetoothHCI 11 | 12 | internal extension L2CAPConnection { 13 | 14 | /// Creates a client socket for an L2CAP connection. 15 | static func lowEnergyClient( 16 | address localAddress: BluetoothAddress, 17 | destination: HCILEAdvertisingReport.Report 18 | ) throws -> Self { 19 | try lowEnergyClient( 20 | address: localAddress, 21 | destination: destination.address, 22 | isRandom: destination.addressType == .random 23 | ) 24 | } 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/GATT/Extensions/OptionSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionSet.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 1/10/25. 6 | // 7 | 8 | extension OptionSet { 9 | @inline(never) 10 | internal func buildDescription( 11 | _ descriptions: [(Element, StaticString)] 12 | ) -> String { 13 | var copy = self 14 | var result = "[" 15 | 16 | for (option, name) in descriptions { 17 | if _slowPath(copy.contains(option)) { 18 | result += name.description 19 | copy.remove(option) 20 | if !copy.isEmpty { result += ", " } 21 | } 22 | } 23 | 24 | if _slowPath(!copy.isEmpty) { 25 | result += "\(Self.self)(rawValue: \(copy.rawValue))" 26 | } 27 | result += "]" 28 | return result 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Swift", 3 | "image": "swift:bookworm", 4 | "features": { 5 | "ghcr.io/devcontainers/features/common-utils:2": { 6 | "installZsh": "false", 7 | "username": "vscode", 8 | "upgradePackages": "false" 9 | }, 10 | "ghcr.io/devcontainers/features/git:1": { 11 | "version": "os-provided", 12 | "ppa": "false" 13 | } 14 | }, 15 | "runArgs": [ 16 | "--cap-add=SYS_PTRACE", 17 | "--security-opt", 18 | "seccomp=unconfined" 19 | ], 20 | // Configure tool-specific properties. 21 | "customizations": { 22 | // Configure properties specific to VS Code. 23 | "vscode": { 24 | // Set *default* container specific settings.json values on container create. 25 | "settings": { 26 | "lldb.library": "/usr/lib/liblldb.so" 27 | }, 28 | // Add the IDs of extensions you want installed when the container is created. 29 | "extensions": [ 30 | "swiftlang.swift-vscode" 31 | ] 32 | } 33 | }, 34 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 35 | // "forwardPorts": [], 36 | 37 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 38 | "remoteUser": "vscode" 39 | } -------------------------------------------------------------------------------- /Sources/DarwinGATT/DarwinCentralError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarwinCentralError.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/14/18. 6 | // 7 | 8 | import Foundation 9 | import Bluetooth 10 | 11 | #if canImport(CoreBluetooth) 12 | 13 | /// Errors for GATT Central Manager 14 | public enum DarwinCentralError: Error { 15 | 16 | /// Bluetooth controller is not enabled. 17 | case invalidState(DarwinBluetoothState) 18 | } 19 | 20 | // MARK: - CustomNSError 21 | 22 | extension DarwinCentralError: CustomNSError { 23 | 24 | public enum UserInfoKey: String { 25 | 26 | /// State 27 | case state = "org.pureswift.DarwinGATT.CentralError.BluetoothState" 28 | } 29 | 30 | public static var errorDomain: String { 31 | return "org.pureswift.DarwinGATT.CentralError" 32 | } 33 | 34 | /// The user-info dictionary. 35 | public var errorUserInfo: [String : Any] { 36 | 37 | var userInfo = [String: Any](minimumCapacity: 2) 38 | 39 | switch self { 40 | 41 | case let .invalidState(state): 42 | 43 | let description = String(format: NSLocalizedString("Invalid state %@.", comment: "org.pureswift.GATT.CentralError.invalidState"), "\(state)") 44 | 45 | userInfo[NSLocalizedDescriptionKey] = description 46 | userInfo[UserInfoKey.state.rawValue] = state 47 | } 48 | 49 | return userInfo 50 | } 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | .DS_Store 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | Packages/ 39 | Package.pins 40 | .swiftpm 41 | .build/ 42 | GATT.xcodeproj/* 43 | *.resolved 44 | .swiftpm 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | 54 | # Carthage 55 | # 56 | 57 | Carthage/* 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/DarwinBluetoothState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarwinBluetoothState.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 6/13/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if canImport(CoreBluetooth) 10 | import Foundation 11 | import CoreBluetooth 12 | 13 | /// Darwin Bluetooth State 14 | /// 15 | /// - SeeAlso: [CBManagerState](https://developer.apple.com/documentation/corebluetooth/cbmanagerstate). 16 | @objc public enum DarwinBluetoothState: Int, Sendable, CaseIterable { 17 | 18 | case unknown 19 | case resetting 20 | case unsupported 21 | case unauthorized 22 | case poweredOff 23 | case poweredOn 24 | } 25 | 26 | // MARK: - CustomStringConvertible 27 | 28 | extension DarwinBluetoothState: CustomStringConvertible { 29 | 30 | public var description: String { 31 | 32 | switch self { 33 | case .unknown: 34 | return "Unknown" 35 | case .resetting: 36 | return "Resetting" 37 | case .unsupported: 38 | return "Unsupported" 39 | case .unauthorized: 40 | return "Unauthorized" 41 | case .poweredOff: 42 | return "Powered Off" 43 | case .poweredOn: 44 | return "Powered On" 45 | } 46 | } 47 | } 48 | 49 | internal extension CBCentralManager { 50 | 51 | var _state: DarwinBluetoothState { 52 | return unsafeBitCast(self.state, to: DarwinBluetoothState.self) 53 | } 54 | } 55 | 56 | internal extension CBPeripheralManager { 57 | 58 | var _state: DarwinBluetoothState { 59 | return unsafeBitCast(self.state, to: DarwinBluetoothState.self) 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/GATT/GATT.docc/GATT.md: -------------------------------------------------------------------------------- 1 | # ``GATT`` 2 | 3 | Bluetooth Generic Attribute Profile (GATT) for Swift 4 | 5 | ## Overview 6 | 7 | The Generic Attributes (GATT) is the name of the interface used to connect to Bluetooth LE devices. The interface has one or more Bluetooth Services, identified by unique ids, that contain Bluetooth Characteristics also identified by ids. 8 | 9 | ## Topics 10 | 11 | ### Central 12 | 13 | The GATT client or central sends requests to a server and receives responses (and server-initiated updates) from it. The GATT client does not know anything in advance about the server’s attributes, so it must first inquire about the presence and nature of those attributes by performing service discovery. After completing service discovery, it can then start reading and writing attributes found in the server, as well as receiving server-initiated updates. 14 | 15 | - ``CentralManager`` 16 | - ``GATTCentral`` 17 | - ``GATTCentralOptions`` 18 | - ``CentralError`` 19 | - ``Peripheral`` 20 | - ``AsyncCentralScan`` 21 | - ``AsyncCentralNotifications`` 22 | - ``ScanData`` 23 | - ``AdvertisementData`` 24 | - ``ManufacturerSpecificData`` 25 | - ``Service`` 26 | - ``Characteristic`` 27 | - ``CharacteristicProperty`` 28 | - ``Descriptor`` 29 | - ``AttributePermission`` 30 | 31 | ### Peripheral 32 | 33 | The GATT server or peripheral receives requests from a client and sends responses back. It also sends server-initiated updates when configured to do so, and it is the role responsible for storing and making the user data available to the client, organized in attributes. 34 | 35 | - ``PeripheralManager`` 36 | - ``GATTPeripheral`` 37 | - ``GATTPeripheralOptions`` 38 | - ``Central`` 39 | - ``GATTReadRequest`` 40 | - ``GATTWriteRequest`` 41 | - ``GATTWriteConfirmation`` 42 | -------------------------------------------------------------------------------- /Sources/GATT/Peer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Peer.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 4/2/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Bluetooth 10 | 11 | /// Bluetooth LE Peer (Central, Peripheral) 12 | public protocol Peer: Hashable, Sendable where ID: Hashable { 13 | 14 | associatedtype ID: Hashable 15 | 16 | /// Unique identifier of the peer. 17 | var id: ID { get } 18 | } 19 | 20 | // MARK: Hashable 21 | 22 | public extension Peer { 23 | 24 | static func == (lhs: Self, rhs: Self) -> Bool { 25 | return lhs.id == rhs.id 26 | } 27 | 28 | func hash(into hasher: inout Hasher) { 29 | id.hash(into: &hasher) 30 | } 31 | } 32 | 33 | // MARK: CustomStringConvertible 34 | 35 | extension Peer where Self: CustomStringConvertible, ID: CustomStringConvertible { 36 | 37 | public var description: String { 38 | return id.description 39 | } 40 | } 41 | 42 | // MARK: - Central 43 | 44 | /// Central Peer 45 | /// 46 | /// Represents a remote central device that has connected to an app implementing the peripheral role on a local device. 47 | public struct Central: Peer, Identifiable, Sendable, CustomStringConvertible { 48 | 49 | public let id: BluetoothAddress 50 | 51 | public init(id: BluetoothAddress) { 52 | self.id = id 53 | } 54 | } 55 | 56 | // MARK: - Peripheral 57 | 58 | /// Peripheral Peer 59 | /// 60 | /// Represents a remote peripheral device that has been discovered. 61 | public struct Peripheral: Peer, Identifiable, Sendable, CustomStringConvertible { 62 | 63 | public let id: BluetoothAddress 64 | 65 | public init(id: BluetoothAddress) { 66 | self.id = id 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | on: [push] 3 | jobs: 4 | 5 | macos: 6 | name: macOS 7 | runs-on: macos-15 8 | strategy: 9 | matrix: 10 | config: ["debug", "release"] 11 | options: ["", "SWIFT_BUILD_DYNAMIC_LIBRARY=1"] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Swift Version 16 | run: swift --version 17 | - name: Build 18 | run: ${{ matrix.options }} swift build -c ${{ matrix.config }} 19 | - name: Test 20 | run: ${{ matrix.options }} swift test -c ${{ matrix.config }} 21 | 22 | linux: 23 | name: Linux 24 | strategy: 25 | matrix: 26 | container: ["swift:6.0.3", "swift:6.1.2"] 27 | config: ["debug", "release"] 28 | options: ["", "SWIFT_BUILD_DYNAMIC_LIBRARY=1"] 29 | runs-on: ubuntu-latest 30 | container: ${{ matrix.container }}-jammy 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Swift Version 35 | run: swift --version 36 | - name: Build 37 | run: ${{ matrix.options }} swift build -c ${{ matrix.config }} 38 | - name: Test 39 | run: ${{ matrix.options }} swift test -c ${{ matrix.config }} 40 | 41 | android: 42 | name: Android 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | swift: ['6.1', 'nightly-6.2'] 47 | arch: ["aarch64", "x86_64"] 48 | runs-on: macos-15 49 | timeout-minutes: 30 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: "Build Swift Package for Android" 53 | run: | 54 | brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip) 55 | skip android sdk install --version ${{ matrix.swift }} 56 | ANDROID_NDK_ROOT="" skip android build --arch ${{ matrix.arch }} -------------------------------------------------------------------------------- /Sources/GATT/ManufacturerSpecificData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManufacturerSpecificData.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 10/23/20. 6 | // 7 | 8 | @_exported import Bluetooth 9 | #if canImport(BluetoothGAP) 10 | @_exported import BluetoothGAP 11 | public typealias ManufacturerSpecificData = GAPManufacturerSpecificData 12 | #else 13 | /// GATT Manufacturer Specific Data 14 | public struct ManufacturerSpecificData : Equatable, Hashable, Sendable { 15 | 16 | internal let data: Data // Optimize for CoreBluetooth / iOS 17 | 18 | public init?(data: Data) { 19 | guard data.count >= 2 20 | else { return nil } 21 | self.data = data 22 | } 23 | } 24 | 25 | public extension ManufacturerSpecificData { 26 | 27 | /// Company Identifier 28 | var companyIdentifier: CompanyIdentifier { 29 | 30 | get { 31 | assert(data.count >= 2, "Invalid manufacturer data") 32 | return CompanyIdentifier(rawValue: UInt16(littleEndian: unsafeBitCast((data[0], data[1]), to: UInt16.self))) 33 | } 34 | 35 | set { self = ManufacturerSpecificData(companyIdentifier: newValue, additionalData: additionalData) } 36 | } 37 | 38 | var additionalData: Data { 39 | 40 | get { 41 | if data.count > 2 { 42 | return Data(data.suffix(from: 2)) 43 | } else { 44 | return Data() 45 | } 46 | } 47 | 48 | set { self = ManufacturerSpecificData(companyIdentifier: companyIdentifier, additionalData: newValue) } 49 | } 50 | 51 | init(companyIdentifier: CompanyIdentifier, 52 | additionalData: Data = Data()) { 53 | 54 | var data = Data() 55 | data.reserveCapacity(2 + additionalData.count) 56 | data += companyIdentifier.rawValue.littleEndian 57 | data += additionalData 58 | self.data = data 59 | } 60 | } 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/GATT/ScanData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanResult.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 1/6/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if canImport(Foundation) 10 | import Foundation 11 | #endif 12 | import Bluetooth 13 | 14 | /// The data for a scan result. 15 | public struct ScanData : Equatable, Hashable, Sendable { 16 | 17 | #if hasFeature(Embedded) 18 | public typealias Timestamp = UInt64 19 | #else 20 | public typealias Timestamp = Foundation.Date 21 | #endif 22 | 23 | /// The discovered peripheral. 24 | public let peripheral: Peripheral 25 | 26 | /// Timestamp for when device was scanned. 27 | public let date: Timestamp 28 | 29 | /// The current received signal strength indicator (RSSI) of the peripheral, in decibels. 30 | public let rssi: Double 31 | 32 | /// Advertisement data. 33 | public let advertisementData: Advertisement 34 | 35 | /// A Boolean value that indicates whether the advertising event type is connectable. 36 | public let isConnectable: Bool 37 | 38 | public init( 39 | peripheral: Peripheral, 40 | date: Timestamp, 41 | rssi: Double, 42 | advertisementData: Advertisement, 43 | isConnectable: Bool 44 | ) { 45 | self.peripheral = peripheral 46 | self.date = date 47 | self.rssi = rssi 48 | self.advertisementData = advertisementData 49 | self.isConnectable = isConnectable 50 | } 51 | } 52 | 53 | // MARK: - Codable 54 | 55 | #if !hasFeature(Embedded) 56 | extension ScanData: Encodable where Peripheral: Encodable, Advertisement: Encodable { } 57 | 58 | extension ScanData: Decodable where Peripheral: Decodable, Advertisement: Decodable { } 59 | #endif 60 | 61 | // MARK: - Identifiable 62 | 63 | extension ScanData: Identifiable { 64 | 65 | public var id: Peripheral.ID { 66 | return peripheral.id 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/Queue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Queue.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 21/12/21. 6 | // 7 | 8 | internal struct Queue { 9 | 10 | private(set) var operations = [Operation]() 11 | 12 | private let execute: (Operation) -> Bool 13 | 14 | init(_ execute: @escaping (Operation) -> Bool) { 15 | self.execute = execute 16 | } 17 | 18 | var current: Operation? { 19 | operations.first 20 | } 21 | 22 | var isEmpty: Bool { 23 | operations.isEmpty 24 | } 25 | 26 | mutating func push(_ operation: Operation) { 27 | operations.append(operation) 28 | // execute immediately if none pending 29 | if operations.count == 1 { 30 | executeCurrent() 31 | } 32 | } 33 | 34 | mutating func pop(_ body: (Operation) -> ()) { 35 | guard let operation = self.current else { 36 | assertionFailure("No pending tasks") 37 | return 38 | } 39 | // finish and remove current 40 | body(operation) 41 | operations.removeFirst() 42 | // execute next 43 | executeCurrent() 44 | } 45 | 46 | mutating func popFirst( 47 | where filter: (Operation) -> (T?), 48 | _ body: (Operation, T) -> () 49 | ) { 50 | for (index, queuedOperation) in operations.enumerated() { 51 | guard let operation = filter(queuedOperation) else { 52 | continue 53 | } 54 | // execute completion 55 | body(queuedOperation, operation) 56 | operations.remove(at: index) 57 | executeCurrent() 58 | return 59 | } 60 | } 61 | 62 | private mutating func executeCurrent() { 63 | if let operation = self.current { 64 | guard execute(operation) else { 65 | operations.removeFirst() 66 | executeCurrent() // execute next 67 | return 68 | } 69 | // wait for continuation 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/GATT/MaximumTransmissionUnit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximumTransmissionUnit.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 10/23/20. 6 | // 7 | 8 | @_exported import Bluetooth 9 | #if canImport(BluetoothGATT) 10 | @_exported import BluetoothGATT 11 | public typealias MaximumTransmissionUnit = ATTMaximumTransmissionUnit 12 | #else 13 | /// GATT Maximum Transmission Unit 14 | public struct MaximumTransmissionUnit: RawRepresentable, Equatable, Hashable, Sendable { 15 | 16 | public let rawValue: UInt16 17 | 18 | public init?(rawValue: UInt16) { 19 | 20 | guard rawValue <= MaximumTransmissionUnit.max.rawValue, 21 | rawValue >= MaximumTransmissionUnit.min.rawValue 22 | else { return nil } 23 | 24 | self.rawValue = rawValue 25 | } 26 | 27 | fileprivate init(_ unsafe: UInt16) { 28 | self.rawValue = unsafe 29 | } 30 | } 31 | 32 | private extension MaximumTransmissionUnit { 33 | 34 | var isValid: Bool { 35 | 36 | return (MaximumTransmissionUnit.min.rawValue ... MaximumTransmissionUnit.max.rawValue).contains(rawValue) 37 | } 38 | } 39 | 40 | public extension MaximumTransmissionUnit { 41 | 42 | static var `default`: MaximumTransmissionUnit { return MaximumTransmissionUnit(23) } 43 | 44 | static var min: MaximumTransmissionUnit { return .default } 45 | 46 | static var max: MaximumTransmissionUnit { return MaximumTransmissionUnit(517) } 47 | 48 | init(server: UInt16, 49 | client: UInt16) { 50 | 51 | let mtu = Swift.min(Swift.max(Swift.min(client, server), MaximumTransmissionUnit.default.rawValue), MaximumTransmissionUnit.max.rawValue) 52 | 53 | self.init(mtu) 54 | 55 | assert(isValid) 56 | } 57 | } 58 | 59 | // MARK: - CustomStringConvertible 60 | 61 | extension MaximumTransmissionUnit: CustomStringConvertible { 62 | 63 | public var description: String { 64 | 65 | return rawValue.description 66 | } 67 | } 68 | 69 | // MARK: - Comparable 70 | 71 | extension MaximumTransmissionUnit: Comparable { 72 | 73 | public static func < (lhs: MaximumTransmissionUnit, rhs: MaximumTransmissionUnit) -> Bool { 74 | 75 | return lhs.rawValue < rhs.rawValue 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/GATT/GATTServerConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Peripheral.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/17/18. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | #endif 11 | #if canImport(BluetoothGATT) 12 | import Bluetooth 13 | import BluetoothGATT 14 | 15 | internal final class GATTServerConnection : @unchecked Sendable { 16 | 17 | typealias Data = Socket.Data 18 | 19 | typealias Error = Socket.Error 20 | 21 | // MARK: - Properties 22 | 23 | public let central: Central 24 | 25 | private let server: GATTServer 26 | 27 | public var maximumUpdateValueLength: Int { 28 | // ATT_MTU-3 29 | Int(server.maximumTransmissionUnit.rawValue) - 3 30 | } 31 | 32 | private let lock = NSLock() 33 | 34 | // MARK: - Initialization 35 | 36 | internal init( 37 | central: Central, 38 | socket: Socket, 39 | maximumTransmissionUnit: ATTMaximumTransmissionUnit, 40 | maximumPreparedWrites: Int, 41 | database: GATTDatabase, 42 | callback: GATTServer.Callback, 43 | log: (@Sendable (String) -> ())? 44 | ) { 45 | self.central = central 46 | self.server = GATTServer( 47 | socket: socket, 48 | maximumTransmissionUnit: maximumTransmissionUnit, 49 | maximumPreparedWrites: maximumPreparedWrites, 50 | database: database, 51 | log: log 52 | ) 53 | self.server.callback = callback 54 | } 55 | 56 | // MARK: - Methods 57 | 58 | /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. 59 | public func write(_ value: Data, forCharacteristic handle: UInt16) { 60 | lock.lock() 61 | defer { lock.unlock() } 62 | server.writeValue(value, forCharacteristic: handle) 63 | } 64 | 65 | public func run() throws(ATTConnectionError) { 66 | lock.lock() 67 | defer { lock.unlock() } 68 | try self.server.run() 69 | } 70 | 71 | public subscript(handle: UInt16) -> Data { 72 | lock.lock() 73 | defer { lock.unlock() } 74 | return server.database[handle: handle].value 75 | } 76 | } 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/GATT/CentralAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CentralAttributes.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 1/11/21. 6 | // 7 | 8 | import Bluetooth 9 | 10 | /// GATT Central Attribute protocol 11 | public protocol GATTCentralAttribute { 12 | 13 | associatedtype Peripheral: Peer 14 | 15 | associatedtype ID 16 | 17 | /// Attribute identifier, usually the ATT handle. 18 | var id: ID { get } 19 | 20 | /// GATT Attribute UUID. 21 | var uuid: BluetoothUUID { get } 22 | 23 | /// Peripheral this attribute was read from. 24 | var peripheral: Peripheral { get } 25 | } 26 | 27 | public struct Service : GATTCentralAttribute, Hashable, Sendable where ID: Sendable { 28 | 29 | public let id: ID 30 | 31 | public let uuid: BluetoothUUID 32 | 33 | public let peripheral: Peripheral 34 | 35 | /// A Boolean value that indicates whether the type of service is primary or secondary. 36 | public let isPrimary: Bool 37 | 38 | public init(id: ID, 39 | uuid: BluetoothUUID, 40 | peripheral: Peripheral, 41 | isPrimary: Bool = true) { 42 | 43 | self.id = id 44 | self.uuid = uuid 45 | self.peripheral = peripheral 46 | self.isPrimary = isPrimary 47 | } 48 | } 49 | 50 | extension Service: Identifiable { } 51 | 52 | public struct Characteristic : GATTCentralAttribute, Hashable, Sendable where ID: Sendable { 53 | 54 | public typealias Properties = CharacteristicProperties 55 | 56 | public let id: ID 57 | 58 | public let uuid: BluetoothUUID 59 | 60 | public let peripheral: Peripheral 61 | 62 | public let properties: Properties 63 | 64 | public init( 65 | id: ID, 66 | uuid: BluetoothUUID, 67 | peripheral: Peripheral, 68 | properties: Properties 69 | ) { 70 | self.id = id 71 | self.uuid = uuid 72 | self.peripheral = peripheral 73 | self.properties = properties 74 | } 75 | } 76 | 77 | extension Characteristic: Identifiable { } 78 | 79 | public struct Descriptor : GATTCentralAttribute, Hashable, Sendable where ID: Sendable { 80 | 81 | public let id: ID 82 | 83 | public let uuid: BluetoothUUID 84 | 85 | public let peripheral: Peripheral 86 | 87 | public init(id: ID, 88 | uuid: BluetoothUUID, 89 | peripheral: Peripheral) { 90 | 91 | self.id = id 92 | self.uuid = uuid 93 | self.peripheral = peripheral 94 | } 95 | } 96 | 97 | extension Descriptor: Identifiable { } 98 | -------------------------------------------------------------------------------- /Sources/GATT/CharacteristicProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacteristicProperty.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 10/23/20. 6 | // 7 | 8 | @_exported import Bluetooth 9 | #if canImport(BluetoothGATT) 10 | @_exported import BluetoothGATT 11 | public typealias CharacteristicProperties = BluetoothGATT.GATTCharacteristicProperties 12 | #else 13 | /// GATT Characteristic Properties Bitfield valuess 14 | public struct CharacteristicProperties: OptionSet, Hashable, Sendable { 15 | 16 | public var rawValue: UInt8 17 | 18 | public init(rawValue: UInt8) { 19 | self.rawValue = rawValue 20 | } 21 | } 22 | 23 | // MARK: - ExpressibleByIntegerLiteral 24 | 25 | extension CharacteristicProperties: ExpressibleByIntegerLiteral { 26 | 27 | public init(integerLiteral value: UInt8) { 28 | self.rawValue = value 29 | } 30 | } 31 | 32 | // MARK: CustomStringConvertible 33 | 34 | extension CharacteristicProperties: CustomStringConvertible, CustomDebugStringConvertible { 35 | 36 | #if hasFeature(Embedded) 37 | public var description: String { 38 | "0x" + rawValue.toHexadecimal() 39 | } 40 | #else 41 | @inline(never) 42 | public var description: String { 43 | let descriptions: [(CharacteristicProperties, StaticString)] = [ 44 | (.broadcast, ".broadcast"), 45 | (.read, ".read"), 46 | (.write, ".write"), 47 | (.notify, ".notify"), 48 | (.indicate, ".indicate"), 49 | (.signedWrite, ".signedWrite"), 50 | (.extendedProperties, ".extendedProperties") 51 | ] 52 | return buildDescription(descriptions) 53 | } 54 | #endif 55 | 56 | /// A textual representation of the file permissions, suitable for debugging. 57 | public var debugDescription: String { self.description } 58 | } 59 | 60 | // MARK: - Options 61 | 62 | public extension CharacteristicProperties { 63 | 64 | static var broadcast: CharacteristicProperties { 0x01 } 65 | static var read: CharacteristicProperties { 0x02 } 66 | static var writeWithoutResponse: CharacteristicProperties { 0x04 } 67 | static var write: CharacteristicProperties { 0x08 } 68 | static var notify: CharacteristicProperties { 0x10 } 69 | static var indicate: CharacteristicProperties { 0x20 } 70 | 71 | /// Characteristic supports write with signature 72 | static var signedWrite: CharacteristicProperties { 0x40 } // BT_GATT_CHRC_PROP_AUTH 73 | 74 | static var extendedProperties: CharacteristicProperties { 0x80 } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /Sources/GATT/AsyncStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncStream.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 4/17/22. 6 | // 7 | 8 | #if !hasFeature(Embedded) 9 | import Foundation 10 | import Bluetooth 11 | 12 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 13 | public struct AsyncCentralScan : AsyncSequence, Sendable { 14 | 15 | public typealias Element = ScanData 16 | 17 | let stream: AsyncIndefiniteStream 18 | 19 | public init( 20 | bufferSize: Int = 100, 21 | _ build: @escaping @Sendable ((Element) -> ()) async throws -> () 22 | ) { 23 | self.stream = .init(bufferSize: bufferSize, build) 24 | } 25 | 26 | public init( 27 | bufferSize: Int = 100, 28 | onTermination: @escaping () -> (), 29 | _ build: (AsyncIndefiniteStream.Continuation) -> () 30 | ) { 31 | self.stream = .init(bufferSize: bufferSize, onTermination: onTermination, build) 32 | } 33 | 34 | public func makeAsyncIterator() -> AsyncIndefiniteStream.AsyncIterator { 35 | stream.makeAsyncIterator() 36 | } 37 | 38 | public func stop() { 39 | stream.stop() 40 | } 41 | 42 | public var isScanning: Bool { 43 | return stream.isExecuting 44 | } 45 | } 46 | 47 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 48 | public extension AsyncCentralScan { 49 | 50 | func first() async throws -> Element? { 51 | for try await element in self { 52 | self.stop() 53 | return element 54 | } 55 | return nil 56 | } 57 | } 58 | 59 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 60 | public struct AsyncCentralNotifications : AsyncSequence, Sendable { 61 | 62 | public typealias Element = Central.Data 63 | 64 | let stream: AsyncIndefiniteStream 65 | 66 | public init( 67 | bufferSize: Int = 100, 68 | _ build: @escaping @Sendable ((Element) -> ()) async throws -> () 69 | ) { 70 | self.stream = .init(bufferSize: bufferSize, build) 71 | } 72 | 73 | public init( 74 | bufferSize: Int = 100, 75 | onTermination: @escaping () -> (), 76 | _ build: (AsyncIndefiniteStream.Continuation) -> () 77 | ) { 78 | self.stream = .init(bufferSize: bufferSize, onTermination: onTermination, build) 79 | } 80 | 81 | public func makeAsyncIterator() -> AsyncIndefiniteStream.AsyncIterator { 82 | stream.makeAsyncIterator() 83 | } 84 | 85 | public func stop() { 86 | stream.stop() 87 | } 88 | 89 | public var isNotifying: Bool { 90 | return stream.isExecuting 91 | } 92 | } 93 | #endif 94 | -------------------------------------------------------------------------------- /Sources/GATT/CentralError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CentralError.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/14/18. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | #endif 11 | import Bluetooth 12 | 13 | /// Errors for GATT Central Manager 14 | public enum CentralError: Error { 15 | 16 | /// Operation timeout. 17 | case timeout 18 | 19 | /// Peripheral is disconnected. 20 | case disconnected 21 | 22 | /// Peripheral from previous scan / unknown. 23 | case unknownPeripheral 24 | 25 | /// The specified attribute was not found. 26 | case invalidAttribute(BluetoothUUID) 27 | } 28 | 29 | // MARK: - CustomNSError 30 | 31 | #if canImport(Darwin) 32 | extension CentralError: CustomNSError { 33 | 34 | public enum UserInfoKey: String { 35 | 36 | /// Bluetooth UUID value (for characteristic or service). 37 | case uuid = "org.pureswift.GATT.CentralError.BluetoothUUID" 38 | } 39 | 40 | public static var errorDomain: String { 41 | return "org.pureswift.GATT.CentralError" 42 | } 43 | 44 | /// The user-info dictionary. 45 | public var errorUserInfo: [String : Any] { 46 | 47 | var userInfo = [String: Any](minimumCapacity: 2) 48 | 49 | switch self { 50 | 51 | case .timeout: 52 | 53 | let description = String(format: NSLocalizedString("The operation timed out.", comment: "org.pureswift.GATT.CentralError.timeout")) 54 | 55 | userInfo[NSLocalizedDescriptionKey] = description 56 | 57 | case .disconnected: 58 | 59 | let description = String(format: NSLocalizedString("The operation cannot be completed becuase the peripheral is disconnected.", comment: "org.pureswift.GATT.CentralError.disconnected")) 60 | 61 | userInfo[NSLocalizedDescriptionKey] = description 62 | 63 | case .unknownPeripheral: 64 | 65 | let description = String(format: NSLocalizedString("Unknown peripheral.", comment: "org.pureswift.GATT.CentralError.unknownPeripheral")) 66 | 67 | userInfo[NSLocalizedDescriptionKey] = description 68 | 69 | case let .invalidAttribute(uuid): 70 | 71 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 72 | let description = String(format: NSLocalizedString("Invalid attribute %@.", comment: "org.pureswift.GATT.CentralError.invalidAttribute"), uuid.description) 73 | #else 74 | let description = "Invalid attribute \(uuid)" 75 | #endif 76 | 77 | userInfo[NSLocalizedDescriptionKey] = description 78 | userInfo[UserInfoKey.uuid.rawValue] = uuid 79 | } 80 | 81 | return userInfo 82 | } 83 | } 84 | #endif 85 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/DarwinAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarwinAttributes.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 4/2/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | // watchOS and tvOS only support Central mode 10 | #if (os(macOS) || os(iOS)) && canImport(BluetoothGATT) 11 | import Foundation 12 | import Bluetooth 13 | import BluetoothGATT 14 | import CoreBluetooth 15 | 16 | internal protocol CoreBluetoothAttributeConvertible { 17 | 18 | associatedtype CoreBluetoothPeripheralType 19 | 20 | func toCoreBluetooth() -> CoreBluetoothPeripheralType 21 | } 22 | 23 | extension GATTAttribute.Service: CoreBluetoothAttributeConvertible where Data == Foundation.Data { 24 | 25 | func toCoreBluetooth() -> CBMutableService { 26 | 27 | let service = CBMutableService(type: CBUUID(uuid), primary: isPrimary) 28 | service.characteristics = characteristics.map { $0.toCoreBluetooth() } 29 | return service 30 | } 31 | } 32 | 33 | extension GATTAttribute.Characteristic: CoreBluetoothAttributeConvertible where Data == Foundation.Data { 34 | 35 | func toCoreBluetooth() -> CBMutableCharacteristic { 36 | 37 | // http://stackoverflow.com/questions/29228244/issues-in-creating-writable-characteristic-in-core-bluetooth-framework#29229075 38 | // Characteristics with cached values must be read-only 39 | // Must set nil as value. 40 | 41 | let characteristic = CBMutableCharacteristic( 42 | type: CBUUID(uuid), 43 | properties: CBCharacteristicProperties(rawValue: .init(properties.rawValue)), 44 | value: nil, 45 | permissions: CBAttributePermissions(rawValue: .init(permissions.rawValue)) 46 | ) 47 | 48 | characteristic.descriptors = descriptors 49 | .filter { CBMutableDescriptor.supportedUUID.contains($0.uuid) } 50 | .map { $0.toCoreBluetooth() } 51 | 52 | return characteristic 53 | } 54 | } 55 | 56 | extension GATTAttribute.Descriptor: CoreBluetoothAttributeConvertible where Data == Foundation.Data { 57 | 58 | func toCoreBluetooth() -> CBMutableDescriptor { 59 | 60 | /* 61 | Only the Characteristic User Description and Characteristic Presentation Format descriptors are currently supported. The Characteristic Extended Properties and Client Characteristic Configuration descriptors will be created automatically upon publication of the parent service, depending on the properties of the characteristic itself 62 | 63 | e.g. 64 | ``` 65 | Assertion failure in -[CBMutableDescriptor initWithType:value:], /SourceCache/CoreBluetooth_Sim/CoreBluetooth-59.3/CBDescriptor.m:25 66 | ``` 67 | */ 68 | 69 | guard let descriptor = CBMutableDescriptor(uuid: self.uuid, data: self.value) 70 | else { fatalError("Unsupported \(CBDescriptor.self) \(uuid)") } 71 | 72 | return descriptor 73 | } 74 | } 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /Sources/GATT/AttributePermission.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributePermission.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 10/23/20. 6 | // 7 | 8 | @_exported import Bluetooth 9 | #if canImport(BluetoothGATT) 10 | @_exported import BluetoothGATT 11 | public typealias AttributePermissions = BluetoothGATT.ATTAttributePermissions 12 | #else 13 | /// ATT attribute permission bitfield values. Permissions are grouped as 14 | /// "Access", "Encryption", "Authentication", and "Authorization". A bitmask of 15 | /// permissions is a byte that encodes a combination of these. 16 | @frozen 17 | public struct AttributePermissions: OptionSet, Equatable, Hashable, Sendable { 18 | 19 | public var rawValue: UInt8 20 | 21 | public init(rawValue: UInt8) { 22 | self.rawValue = rawValue 23 | } 24 | } 25 | 26 | // MARK: - ExpressibleByIntegerLiteral 27 | 28 | extension AttributePermissions: ExpressibleByIntegerLiteral { 29 | 30 | public init(integerLiteral value: UInt8) { 31 | self.rawValue = value 32 | } 33 | } 34 | 35 | // MARK: - CustomStringConvertible 36 | 37 | extension AttributePermissions: CustomStringConvertible, CustomDebugStringConvertible { 38 | 39 | #if hasFeature(Embedded) 40 | public var description: String { 41 | "0x" + rawValue.toHexadecimal() 42 | } 43 | #else 44 | @inline(never) 45 | public var description: String { 46 | let descriptions: [(AttributePermissions, StaticString)] = [ 47 | (.read, ".read"), 48 | (.write, ".write"), 49 | (.readEncrypt, ".readEncrypt"), 50 | (.writeEncrypt, ".writeEncrypt"), 51 | (.readAuthentication, ".readAuthentication"), 52 | (.writeAuthentication, ".writeAuthentication"), 53 | (.authorized, ".authorized"), 54 | (.noAuthorization, ".noAuthorization"), 55 | ] 56 | return buildDescription(descriptions) 57 | } 58 | #endif 59 | 60 | /// A textual representation of the file permissions, suitable for debugging. 61 | public var debugDescription: String { self.description } 62 | } 63 | 64 | // MARK: - Options 65 | 66 | public extension AttributePermissions { 67 | 68 | // Access 69 | static var read: AttributePermissions { 0x01 } 70 | static var write: AttributePermissions { 0x02 } 71 | 72 | // Encryption 73 | static var encrypt: AttributePermissions { [.readEncrypt, .writeEncrypt] } 74 | static var readEncrypt: AttributePermissions { 0x04 } 75 | static var writeEncrypt: AttributePermissions { 0x08 } 76 | 77 | // The following have no effect on Darwin 78 | 79 | // Authentication 80 | static var authentication: AttributePermissions { [.readAuthentication, .writeAuthentication] } 81 | static var readAuthentication: AttributePermissions { 0x10 } 82 | static var writeAuthentication: AttributePermissions { 0x20 } 83 | 84 | // Authorization 85 | static var authorized: AttributePermissions { 0x40 } 86 | static var noAuthorization: AttributePermissions { 0x80 } 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /.github/workflows/swift-arm.yml: -------------------------------------------------------------------------------- 1 | name: Swift ARM 2 | on: [push] 3 | jobs: 4 | 5 | linux-arm-raspios-build: 6 | name: Linux (Raspios) 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | arch: ["armv6", "armv7"] 11 | swift: ["6.1.2"] 12 | config: ["debug" , "release"] 13 | linux: ["raspios"] 14 | release: ["bookworm"] 15 | container: swift:${{ matrix.swift }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Install dependencies 20 | run: apt update -y; apt install wget -y 21 | - name: Install SDK 22 | run: | 23 | wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz 24 | tar -xvf swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz 25 | mv swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} 26 | - name: Swift Version 27 | run: swift --version 28 | - name: Build 29 | run: SWIFT_BUILD_DYNAMIC_LIBRARY=1 swift build -c ${{ matrix.config }} --destination /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}/${{ matrix.linux }}-${{ matrix.release }}.json 30 | 31 | linux-arm-debian-build: 32 | name: Linux (Debian) 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | arch: ["armv7"] 37 | swift: ["6.1.2"] 38 | config: ["debug" , "release"] 39 | linux: ["debian"] 40 | release: ["bookworm", "bullseye"] 41 | container: swift:${{ matrix.swift }} 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | - name: Install dependencies 46 | run: apt update -y; apt install wget -y 47 | - name: Install SDK 48 | run: | 49 | wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz 50 | tar -xvf swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz 51 | mv swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} 52 | - name: Swift Version 53 | run: swift --version 54 | - name: Build 55 | run: SWIFT_BUILD_DYNAMIC_LIBRARY=1 swift build -c ${{ matrix.config }} --destination /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}/${{ matrix.linux }}-${{ matrix.release }}.json 56 | - name: Upload artifacts 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: "swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-coremodel-${{ matrix.config }}" 60 | path: .build/armv7-unknown-linux-gnueabihf/${{ matrix.config }}/libGATT.so 61 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/PeripheralContinuation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralContinuation.wift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 20/12/21. 6 | // 7 | 8 | #if canImport(CoreBluetooth) 9 | import Foundation 10 | import Bluetooth 11 | import GATT 12 | 13 | #if DEBUG 14 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 15 | internal struct PeripheralContinuation where T: Sendable, E: Error { 16 | 17 | private let function: String 18 | 19 | private let continuation: CheckedContinuation 20 | 21 | private let peripheral: DarwinCentral.Peripheral 22 | 23 | fileprivate init( 24 | continuation: UnsafeContinuation, 25 | function: String, 26 | peripheral: DarwinCentral.Peripheral 27 | ) { 28 | self.continuation = CheckedContinuation(continuation: continuation, function: function) 29 | self.function = function 30 | self.peripheral = peripheral 31 | } 32 | 33 | func resume( 34 | returning value: T 35 | ) { 36 | continuation.resume(returning: value) 37 | } 38 | 39 | func resume( 40 | throwing error: E 41 | ) { 42 | continuation.resume(throwing: error) 43 | } 44 | 45 | func resume( 46 | with result: Result 47 | ) { 48 | continuation.resume(with: result) 49 | } 50 | } 51 | 52 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 53 | extension PeripheralContinuation where T == Void { 54 | 55 | func resume() { 56 | self.resume(returning: ()) 57 | } 58 | } 59 | 60 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 61 | internal func withContinuation( 62 | for peripheral: DarwinCentral.Peripheral, 63 | function: String = #function, 64 | _ body: (PeripheralContinuation) -> Void 65 | ) async -> T { 66 | return await withUnsafeContinuation { 67 | body(PeripheralContinuation(continuation: $0, function: function, peripheral: peripheral)) 68 | } 69 | } 70 | 71 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 72 | internal func withThrowingContinuation( 73 | for peripheral: DarwinCentral.Peripheral, 74 | function: String = #function, 75 | _ body: (PeripheralContinuation) -> Void 76 | ) async throws -> T { 77 | return try await withUnsafeThrowingContinuation { 78 | body(PeripheralContinuation(continuation: $0, function: function, peripheral: peripheral)) 79 | } 80 | } 81 | #else 82 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 83 | internal typealias PeripheralContinuation = CheckedContinuation where E: Error 84 | 85 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 86 | @inline(__always) 87 | internal func withContinuation( 88 | for peripheral: DarwinCentral.Peripheral, 89 | function: String = #function, 90 | _ body: (CheckedContinuation) -> Void 91 | ) async -> T { 92 | return await withCheckedContinuation(function: function, body) 93 | } 94 | 95 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 96 | @inline(__always) 97 | internal func withThrowingContinuation( 98 | for peripheral: DarwinCentral.Peripheral, 99 | function: String = #function, 100 | _ body: (CheckedContinuation) -> Void 101 | ) async throws -> T { 102 | return try await withCheckedThrowingContinuation(function: function, body) 103 | } 104 | #endif 105 | #endif 106 | -------------------------------------------------------------------------------- /Sources/GATT/CentralProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Central.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 4/3/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #if !hasFeature(Embedded) 10 | import Bluetooth 11 | 12 | /// GATT Central Manager 13 | /// 14 | /// Implementation varies by operating system and framework. 15 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 16 | public protocol CentralManager: AnyObject { 17 | 18 | /// Central Peripheral Type 19 | associatedtype Peripheral: Peer 20 | 21 | /// Central Advertisement Type 22 | associatedtype Advertisement: AdvertisementData 23 | 24 | /// Central Attribute ID (Handle) 25 | associatedtype AttributeID: Hashable 26 | 27 | associatedtype Data: DataContainer 28 | 29 | /// Logging 30 | var log: (@Sendable (String) -> ())? { get set } 31 | 32 | /// Currently scanned devices, or restored devices. 33 | var peripherals: [Peripheral: Bool] { get async } 34 | 35 | /// Scans for peripherals that are advertising services. 36 | func scan(filterDuplicates: Bool) async throws -> AsyncCentralScan 37 | 38 | /// Disconnected peripheral callback 39 | //var didDisconnect: AsyncStream { get } 40 | 41 | /// Connect to the specified device 42 | func connect(to peripheral: Peripheral) async throws 43 | 44 | /// Disconnect the specified device. 45 | func disconnect(_ peripheral: Peripheral) async 46 | 47 | /// Disconnect all connected devices. 48 | func disconnectAll() async 49 | 50 | /// Discover Services 51 | func discoverServices( 52 | _ services: Set, 53 | for peripheral: Peripheral 54 | ) async throws -> [Service] 55 | 56 | /// Discover Characteristics for service 57 | func discoverCharacteristics( 58 | _ characteristics: Set, 59 | for service: Service 60 | ) async throws -> [Characteristic] 61 | 62 | /// Read Characteristic Value 63 | func readValue( 64 | for characteristic: Characteristic 65 | ) async throws -> Data 66 | 67 | /// Write Characteristic Value 68 | func writeValue( 69 | _ data: Data, 70 | for characteristic: Characteristic, 71 | withResponse: Bool 72 | ) async throws 73 | 74 | /// Discover descriptors 75 | func discoverDescriptors( 76 | for characteristic: Characteristic 77 | ) async throws -> [Descriptor] 78 | 79 | /// Read descriptor 80 | func readValue( 81 | for descriptor: Descriptor 82 | ) async throws -> Data 83 | 84 | /// Write descriptor 85 | func writeValue( 86 | _ data: Data, 87 | for descriptor: Descriptor 88 | ) async throws 89 | 90 | /// Start Notifications 91 | func notify( 92 | for characteristic: Characteristic 93 | ) async throws -> AsyncCentralNotifications 94 | 95 | /// Read MTU 96 | func maximumTransmissionUnit(for peripheral: Peripheral) async throws -> MaximumTransmissionUnit 97 | 98 | // Read RSSI 99 | func rssi(for peripheral: Peripheral) async throws -> RSSI 100 | } 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import PackageDescription 3 | import class Foundation.ProcessInfo 4 | 5 | // force building as dynamic library 6 | let dynamicLibrary = ProcessInfo.processInfo.environment["SWIFT_BUILD_DYNAMIC_LIBRARY"] != nil 7 | let libraryType: PackageDescription.Product.Library.LibraryType? = dynamicLibrary ? .dynamic : nil 8 | 9 | var package = Package( 10 | name: "GATT", 11 | platforms: [ 12 | .macOS(.v10_15), 13 | .iOS(.v13), 14 | .watchOS(.v6), 15 | .tvOS(.v13), 16 | ], 17 | products: [ 18 | .library( 19 | name: "GATT", 20 | type: libraryType, 21 | targets: ["GATT"] 22 | ), 23 | .library( 24 | name: "DarwinGATT", 25 | type: libraryType, 26 | targets: ["DarwinGATT"] 27 | ) 28 | ], 29 | dependencies: [ 30 | .package( 31 | url: "https://github.com/PureSwift/Bluetooth.git", 32 | from: "7.2.0" 33 | ) 34 | ], 35 | targets: [ 36 | .target( 37 | name: "GATT", 38 | dependencies: [ 39 | .product( 40 | name: "Bluetooth", 41 | package: "Bluetooth" 42 | ), 43 | .product( 44 | name: "BluetoothGATT", 45 | package: "Bluetooth", 46 | condition: .when(platforms: [.macOS, .linux]) 47 | ), 48 | .product( 49 | name: "BluetoothGAP", 50 | package: "Bluetooth", 51 | condition: .when(platforms: [.macOS, .linux, .android]) 52 | ), 53 | .product( 54 | name: "BluetoothHCI", 55 | package: "Bluetooth", 56 | condition: .when(platforms: [.macOS, .linux]) 57 | ) 58 | ] 59 | ), 60 | .target( 61 | name: "DarwinGATT", 62 | dependencies: [ 63 | "GATT", 64 | .product( 65 | name: "BluetoothGATT", 66 | package: "Bluetooth", 67 | condition: .when(platforms: [.macOS]) 68 | ) 69 | ], 70 | swiftSettings: [.swiftLanguageMode(.v5)] 71 | ), 72 | .testTarget( 73 | name: "GATTTests", 74 | dependencies: [ 75 | "GATT", 76 | .product( 77 | name: "Bluetooth", 78 | package: "Bluetooth" 79 | ), 80 | .product( 81 | name: "BluetoothGATT", 82 | package: "Bluetooth", 83 | condition: .when(platforms: [.macOS, .linux]) 84 | ), 85 | .product( 86 | name: "BluetoothGAP", 87 | package: "Bluetooth", 88 | condition: .when(platforms: [.macOS, .linux]) 89 | ), 90 | .product( 91 | name: "BluetoothHCI", 92 | package: "Bluetooth", 93 | condition: .when(platforms: [.macOS, .linux]) 94 | ) 95 | ], 96 | swiftSettings: [.swiftLanguageMode(.v5)] 97 | ) 98 | ] 99 | ) 100 | 101 | // SwiftPM command plugins are only supported by Swift version 5.6 and later. 102 | let buildDocs = ProcessInfo.processInfo.environment["BUILDING_FOR_DOCUMENTATION_GENERATION"] != nil 103 | if buildDocs { 104 | package.dependencies += [ 105 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/DarwinAdvertisementData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarwinAdvertisementData.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/15/18. 6 | // 7 | 8 | @preconcurrency import Foundation 9 | import Bluetooth 10 | import GATT 11 | 12 | #if canImport(CoreBluetooth) 13 | import CoreBluetooth 14 | 15 | /// CoreBluetooth Adverisement Data 16 | public struct DarwinAdvertisementData: AdvertisementData { 17 | 18 | public typealias Data = Foundation.Data 19 | 20 | // MARK: - Properties 21 | 22 | internal let data: [String: NSObject] 23 | 24 | // MARK: - Initialization 25 | 26 | internal init(_ coreBluetooth: [String: Any]) { 27 | 28 | guard let data = coreBluetooth as? [String: NSObject] 29 | else { fatalError("Invalid dictionary \(coreBluetooth)") } 30 | 31 | self.data = data 32 | } 33 | } 34 | 35 | // MARK: - Equatable 36 | 37 | extension DarwinAdvertisementData: Equatable { 38 | 39 | public static func == (lhs: DarwinAdvertisementData, rhs: DarwinAdvertisementData) -> Bool { 40 | return lhs.data == rhs.data 41 | } 42 | } 43 | 44 | // MARK: - Hashable 45 | 46 | extension DarwinAdvertisementData: Hashable { 47 | 48 | public func hash(into hasher: inout Hasher) { 49 | data.hash(into: &hasher) 50 | } 51 | } 52 | 53 | // MARK: - CustomStringConvertible 54 | 55 | extension DarwinAdvertisementData: CustomStringConvertible { 56 | 57 | public var description: String { 58 | return (data as NSDictionary).description 59 | } 60 | } 61 | 62 | // MARK: - AdvertisementData 63 | 64 | public extension DarwinAdvertisementData { 65 | 66 | /// The local name of a peripheral. 67 | var localName: String? { 68 | 69 | return data[CBAdvertisementDataLocalNameKey] as? String 70 | } 71 | 72 | /// The Manufacturer data of a peripheral. 73 | var manufacturerData: ManufacturerSpecificData? { 74 | 75 | guard let manufacturerDataBytes = data[CBAdvertisementDataManufacturerDataKey] as? Data, 76 | let manufacturerData = ManufacturerSpecificData(data: manufacturerDataBytes) 77 | else { return nil } 78 | 79 | return manufacturerData 80 | } 81 | 82 | /// Service-specific advertisement data. 83 | var serviceData: [BluetoothUUID: Data]? { 84 | 85 | guard let coreBluetoothServiceData = data[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] 86 | else { return nil } 87 | 88 | var serviceData = [BluetoothUUID: Data](minimumCapacity: coreBluetoothServiceData.count) 89 | 90 | for (key, value) in coreBluetoothServiceData { 91 | 92 | let uuid = BluetoothUUID(key) 93 | 94 | serviceData[uuid] = value 95 | } 96 | 97 | return serviceData 98 | } 99 | 100 | /// An array of service UUIDs 101 | var serviceUUIDs: [BluetoothUUID]? { 102 | 103 | return (data[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID])?.map { BluetoothUUID($0) } 104 | } 105 | 106 | /// This value is available if the broadcaster (peripheral) provides its Tx power level in its advertising packet. 107 | /// Using the RSSI value and the Tx power level, it is possible to calculate path loss. 108 | var txPowerLevel: Double? { 109 | 110 | return (data[CBAdvertisementDataTxPowerLevelKey] as? NSNumber)?.doubleValue 111 | } 112 | 113 | /// An array of one or more `BluetoothUUID`, representing Service UUIDs. 114 | var solicitedServiceUUIDs: [BluetoothUUID]? { 115 | 116 | return (data[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID])?.map { BluetoothUUID($0) } 117 | } 118 | 119 | // MARK: - CoreBluetooth Specific Values 120 | 121 | /// A Boolean value that indicates whether the advertising event type is connectable. 122 | internal var isConnectable: Bool? { 123 | 124 | return (data[CBAdvertisementDataIsConnectable] as? NSNumber)?.boolValue 125 | } 126 | 127 | /// An array of one or more `BluetoothUUID`, representing Service UUIDs that were found 128 | /// in the “overflow” area of the advertisement data. 129 | var overflowServiceUUIDs: [BluetoothUUID]? { 130 | 131 | return (data[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID])?.map { BluetoothUUID($0) } 132 | } 133 | } 134 | 135 | #endif 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GATT 2 | 3 | [![Swift][swift-badge]][swift-url] 4 | [![Platform][platform-badge]][platform-url] 5 | [![Release][release-badge]][release-url] 6 | [![License][mit-badge]][mit-url] 7 | 8 | Bluetooth Generic Attribute Profile (GATT) for Swift 9 | 10 | ## Installation 11 | 12 | GATT is available as a Swift Package Manager package. To use it, add the following dependency in your `Package.swift`: 13 | 14 | ```swift 15 | .package(url: "https://github.com/PureSwift/GATT.git", branch: "master"), 16 | ``` 17 | 18 | and to your target, add `GATT` to your dependencies. You can then `import GATT` to get access to GATT functionality. 19 | 20 | ## Platforms 21 | 22 | | Platform | Roles | Backend | Library | 23 | | ---- | -------- | --- | ----------- | 24 | | macOS, iOS, watchOS, tvOS, visionOS | Central, Peripheral | [CoreBluetooth](https://developer.apple.com/documentation/corebluetooth) | [DarwinGATT](https://github.com/PureSwift/GATT) | 25 | | Linux | Central, Peripheral | [BlueZ](https://www.bluez.org) | [BluetoothLinux](https://github.com/PureSwift/BluetoothLinux), [GATT](https://github.com/PureSwift/GATT) 26 | | Android | Central | [Java Native Interface](https://developer.android.com/training/articles/perf-jni) | [AndroidBluetooth](https://github.com/PureSwift/AndroidBluetooth) 27 | | WebAssembly | Central | [Bluetooth Web API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) | [BluetoothWeb](https://github.com/PureSwift/BluetoothWeb) 28 | | Pi Pico W | Peripheral | [BlueKitchen BTStack](https://bluekitchen-gmbh.com/btstack/#quick_start/index.html) | [BTStack](https://github.com/MillerTechnologyPeru/BTStack) 29 | | ESP32 | Peripheral | [Apache NimBLE](https://mynewt.apache.org/latest/network/index.html) | [NimBLE](https://github.com/MillerTechnologyPeru/NimBLE) 30 | | nRF52840 | Peripheral | [Zephyr SDK](https://zephyrproject.org) | [Zephyr](https://github.com/MillerTechnologyPeru/Zephyr-Swift) 31 | 32 | ## Usage 33 | 34 | ### Peripheral 35 | 36 | ```swift 37 | import Bluetooth 38 | #if canImport(Darwin) 39 | import DarwinGATT 40 | #elseif os(Linux) 41 | import BluetoothLinux 42 | #endif 43 | 44 | #if os(Linux) 45 | typealias LinuxPeripheral = GATTPeripheral 46 | guard let hostController = await HostController.default else { 47 | fatalError("No Bluetooth hardware connected") 48 | } 49 | let serverOptions = GATTPeripheralOptions( 50 | maximumTransmissionUnit: .max, 51 | maximumPreparedWrites: 1000 52 | ) 53 | let peripheral = LinuxPeripheral( 54 | hostController: hostController, 55 | options: serverOptions, 56 | socket: BluetoothLinux.L2CAPSocket.self 57 | ) 58 | #elseif canImport(Darwin) 59 | let peripheral = DarwinPeripheral() 60 | #else 61 | #error("Unsupported platform") 62 | #endif 63 | 64 | // start advertising 65 | try await peripheral.start() 66 | 67 | ``` 68 | 69 | ### Central 70 | 71 | ```swift 72 | import Bluetooth 73 | #if canImport(Darwin) 74 | import DarwinGATT 75 | #elseif os(Linux) 76 | import BluetoothLinux 77 | #endif 78 | 79 | #if os(Linux) 80 | typealias LinuxCentral = GATTCentral 81 | let hostController = await HostController.default 82 | let central = LinuxCentral( 83 | hostController: hostController, 84 | socket: BluetoothLinux.L2CAPSocket.self 85 | ) 86 | #elseif canImport(Darwin) 87 | let central = DarwinCentral() 88 | #else 89 | #error("Unsupported platform") 90 | #endif 91 | 92 | // start scanning 93 | let stream = try await central.scan(filterDuplicates: true) 94 | for try await scanData in stream { 95 | print(scanData) 96 | stream.stop() 97 | } 98 | 99 | ``` 100 | 101 | ## Documentation 102 | 103 | Read the documentation [here](http://pureswift.github.io/GATT/documentation/gatt/). 104 | Documentation can be generated with [DocC](https://github.com/apple/swift-docc). 105 | 106 | License 107 | ------- 108 | 109 | **GATT** is released under the MIT license. See LICENSE for details. 110 | 111 | [swift-badge]: https://img.shields.io/badge/swift-6.0-F05138.svg "Swift 6.0" 112 | [swift-url]: https://swift.org 113 | [platform-badge]: https://img.shields.io/badge/platform-macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Linux%20%7C%20Android-lightgrey.svg 114 | [platform-url]: https://swift.org 115 | [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 116 | [mit-url]: https://tldrlegal.com/license/mit-license 117 | [build-status-badge]: https://github.com/PureSwift/GATT/workflows/Swift/badge.svg 118 | [build-status-url]: https://github.com/PureSwift/GATT/actions 119 | [release-badge]: https://img.shields.io/github/release/PureSwift/GATT.svg 120 | [release-url]: https://github.com/PureSwift/GATT/releases 121 | [docs-url]: http://pureswift.github.io/GATT/documentation/GATT/ 122 | -------------------------------------------------------------------------------- /Tests/GATTTests/TestHostController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostController.swift 3 | // BluetoothTests 4 | // 5 | // Created by Alsey Coleman Miller on 3/29/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if canImport(BluetoothHCI) 10 | import Foundation 11 | import Bluetooth 12 | import BluetoothHCI 13 | 14 | final class TestHostController: BluetoothHostControllerInterface, @unchecked Sendable { 15 | 16 | typealias Data = Foundation.Data 17 | 18 | /// All controllers on the host. 19 | static var controllers: [TestHostController] { return [TestHostController(address: .min)] } 20 | 21 | private(set) var isAdvertising: Bool = false { 22 | didSet { log?("Advertising \(isAdvertising ? "Enabled" : "Disabled")") } 23 | } 24 | 25 | private(set) var isScanning: Bool = false { 26 | didSet { log?("Scanning \(isScanning ? "Enabled" : "Disabled")") } 27 | } 28 | 29 | var advertisingReports = [Data]() 30 | 31 | var log: ((String) -> ())? 32 | 33 | /// The Bluetooth Address of the controller. 34 | let address: BluetoothAddress 35 | 36 | init(address: BluetoothAddress) { 37 | self.address = address 38 | self.log = { print("HCI \(address):", $0) } 39 | } 40 | 41 | /// Send an HCI command to the controller. 42 | func deviceCommand (_ command: T) throws { fatalError() } 43 | 44 | /// Send an HCI command with parameters to the controller. 45 | func deviceCommand (_ commandParameter: T) throws { fatalError() } 46 | 47 | /// Send a command to the controller and wait for response. 48 | func deviceRequest(_ command: C, timeout: HCICommandTimeout) throws { fatalError() } 49 | 50 | /// Send a command to the controller and wait for response. 51 | func deviceRequest (_ commandParameter: CP, timeout: HCICommandTimeout) throws { 52 | 53 | if let command = commandParameter as? HCILESetAdvertiseEnable { 54 | // check if already enabled 55 | guard isAdvertising == command.isEnabled 56 | else { throw HCIError.commandDisallowed } 57 | // set new value 58 | self.isAdvertising = command.isEnabled 59 | } 60 | else if let command = commandParameter as? HCILESetScanEnable { 61 | // check if already enabled 62 | guard isScanning == command.isEnabled 63 | else { throw HCIError.commandDisallowed } 64 | // set new value 65 | self.isScanning = command.isEnabled 66 | } else { 67 | 68 | } 69 | } 70 | 71 | func deviceRequest(_ command: C, 72 | _ eventParameterType: EP.Type, 73 | timeout: HCICommandTimeout) throws -> EP { 74 | fatalError() 75 | } 76 | 77 | /// Sends a command to the device and waits for a response. 78 | func deviceRequest (_ commandParameter: CP, 79 | _ eventParameterType: EP.Type, 80 | timeout: HCICommandTimeout) throws -> EP { 81 | 82 | fatalError() 83 | } 84 | 85 | /// Sends a command to the device and waits for a response with return parameter values. 86 | func deviceRequest (_ commandReturnType : Return.Type, timeout: HCICommandTimeout) throws -> Return { 87 | if commandReturnType == HCIReadDeviceAddress.self { 88 | return HCIReadDeviceAddress(data: Data(self.address.littleEndian))! as! Return 89 | } 90 | fatalError("\(commandReturnType) not mocked") 91 | } 92 | 93 | /// Sends a command to the device and waits for a response with return parameter values. 94 | func deviceRequest (_ commandParameter: CP, _ commandReturnType : Return.Type, timeout: HCICommandTimeout) throws -> Return { 95 | 96 | assert(CP.command.opcode == Return.command.opcode) 97 | fatalError() 98 | } 99 | 100 | /// Polls and waits for events. 101 | func receive(_ eventType: Event.Type) async throws -> Event where Event : BluetoothHCI.HCIEventParameter, Event.HCIEventType == BluetoothHCI.HCIGeneralEvent { 102 | 103 | guard eventType == HCILowEnergyMetaEvent.self 104 | else { fatalError("Invalid event parameter type") } 105 | 106 | while self.advertisingReports.isEmpty { 107 | try await Task.sleep(nanoseconds: 100_000_000) 108 | } 109 | 110 | guard let eventBuffer = self.advertisingReports.popFirst() else { 111 | fatalError() 112 | } 113 | 114 | let actualBytesRead = eventBuffer.count 115 | let eventHeader = HCIEventHeader(data: Data(eventBuffer[0 ..< HCIEventHeader.length])) 116 | let eventData = Data(eventBuffer[HCIEventHeader.length ..< actualBytesRead]) 117 | 118 | guard let eventParameter = Event.init(data: eventData) 119 | else { throw BluetoothHostControllerError.garbageResponse(Data(eventData)) } 120 | 121 | assert(eventHeader?.event.rawValue == Event.event.rawValue) 122 | return eventParameter 123 | } 124 | 125 | 126 | } 127 | 128 | internal extension Array { 129 | 130 | mutating func popFirst() -> Element? { 131 | guard isEmpty == false else { return nil } 132 | return removeFirst() 133 | } 134 | } 135 | #endif 136 | -------------------------------------------------------------------------------- /Sources/GATT/PeripheralProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Peripheral.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 4/1/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #if canImport(BluetoothGATT) 10 | @_exported import Bluetooth 11 | @_exported import BluetoothGATT 12 | 13 | /// GATT Peripheral Manager 14 | /// 15 | /// Implementation varies by operating system. 16 | public protocol PeripheralManager { 17 | 18 | /// Central Peer 19 | /// 20 | /// Represents a remote central device that has connected to an app implementing the peripheral role on a local device. 21 | associatedtype Central: Peer 22 | 23 | associatedtype Data: DataContainer 24 | 25 | associatedtype Error: Swift.Error 26 | 27 | var log: (@Sendable (String) -> ())? { get set } 28 | 29 | /// Start advertising the peripheral and listening for incoming connections. 30 | func start() throws(Error) 31 | 32 | /// Stop the peripheral. 33 | func stop() 34 | 35 | /// A Boolean value that indicates whether the peripheral is advertising data. 36 | var isAdvertising: Bool { get } 37 | 38 | /// Attempts to add the specified service to the GATT database. 39 | /// 40 | /// - Returns: Handle for service declaration and handles for characteristic value handles. 41 | func add(service: BluetoothGATT.GATTAttribute.Service) throws(Error) -> (UInt16, [UInt16]) 42 | 43 | /// Removes the service with the specified handle. 44 | func remove(service: UInt16) 45 | 46 | /// Clears the local GATT database. 47 | func removeAllServices() 48 | 49 | /// Callback to handle GATT read requests. 50 | var willRead: ((GATTReadRequest) -> ATTError?)? { get set } 51 | 52 | /// Callback to handle GATT write requests. 53 | var willWrite: ((GATTWriteRequest) -> ATTError?)? { get set } 54 | 55 | /// Callback to handle post-write actions for GATT write requests. 56 | var didWrite: ((GATTWriteConfirmation) -> ())? { get set } 57 | 58 | /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. 59 | func write(_ newValue: Data, forCharacteristic handle: UInt16) 60 | 61 | /// Modify the value of a characteristic, optionally emiting notifications if configured on the specified connection. 62 | /// 63 | /// Throws error if central is unknown or disconnected. 64 | func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) throws(Error) 65 | 66 | /// Read the value of the characteristic with specified handle. 67 | subscript(characteristic handle: UInt16) -> Data { get } 68 | 69 | /// Read the value of the characteristic with specified handle for the specified connection. 70 | func value(for characteristicHandle: UInt16, central: Central) throws(Error) -> Data 71 | } 72 | 73 | // MARK: - Supporting Types 74 | 75 | public protocol GATTRequest { 76 | 77 | associatedtype Central: Peer 78 | 79 | associatedtype Data: DataContainer 80 | 81 | var central: Central { get } 82 | 83 | var maximumUpdateValueLength: Int { get } 84 | 85 | var uuid: BluetoothUUID { get } 86 | 87 | var handle: UInt16 { get } 88 | 89 | var value: Data { get } 90 | } 91 | 92 | public struct GATTReadRequest : GATTRequest, Equatable, Hashable, Sendable { 93 | 94 | public let central: Central 95 | 96 | public let maximumUpdateValueLength: Int 97 | 98 | public let uuid: BluetoothUUID 99 | 100 | public let handle: UInt16 101 | 102 | public let value: Data 103 | 104 | public let offset: Int 105 | 106 | public init(central: Central, 107 | maximumUpdateValueLength: Int, 108 | uuid: BluetoothUUID, 109 | handle: UInt16, 110 | value: Data, 111 | offset: Int) { 112 | 113 | self.central = central 114 | self.maximumUpdateValueLength = maximumUpdateValueLength 115 | self.uuid = uuid 116 | self.handle = handle 117 | self.value = value 118 | self.offset = offset 119 | } 120 | } 121 | 122 | public struct GATTWriteRequest : GATTRequest, Equatable, Hashable, Sendable { 123 | 124 | public let central: Central 125 | 126 | public let maximumUpdateValueLength: Int 127 | 128 | public let uuid: BluetoothUUID 129 | 130 | public let handle: UInt16 131 | 132 | public let value: Data 133 | 134 | public let newValue: Data 135 | 136 | public init(central: Central, 137 | maximumUpdateValueLength: Int, 138 | uuid: BluetoothUUID, 139 | handle: UInt16, 140 | value: Data, 141 | newValue: Data) { 142 | 143 | self.central = central 144 | self.maximumUpdateValueLength = maximumUpdateValueLength 145 | self.uuid = uuid 146 | self.handle = handle 147 | self.value = value 148 | self.newValue = newValue 149 | } 150 | } 151 | 152 | public struct GATTWriteConfirmation : GATTRequest, Equatable, Hashable, Sendable { 153 | 154 | public let central: Central 155 | 156 | public let maximumUpdateValueLength: Int 157 | 158 | public let uuid: BluetoothUUID 159 | 160 | public let handle: UInt16 161 | 162 | public let value: Data 163 | 164 | public init(central: Central, 165 | maximumUpdateValueLength: Int, 166 | uuid: BluetoothUUID, 167 | handle: UInt16, 168 | value: Data) { 169 | 170 | self.central = central 171 | self.maximumUpdateValueLength = maximumUpdateValueLength 172 | self.uuid = uuid 173 | self.handle = handle 174 | self.value = value 175 | } 176 | } 177 | 178 | #endif 179 | -------------------------------------------------------------------------------- /Sources/GATT/AdvertisementData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvertisementData.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 3/9/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | @_exported import Bluetooth 10 | #if canImport(BluetoothGAP) 11 | import BluetoothGAP 12 | #endif 13 | 14 | /// GATT Advertisement Data. 15 | public protocol AdvertisementData: Hashable, Sendable { 16 | 17 | associatedtype Data where Data: DataContainer 18 | 19 | /// The local name of a peripheral. 20 | var localName: String? { get } 21 | 22 | /// The Manufacturer data of a peripheral. 23 | var manufacturerData: ManufacturerSpecificData? { get } 24 | 25 | /// This value is available if the broadcaster (peripheral) provides its Tx power level in its advertising packet. 26 | /// Using the RSSI value and the Tx power level, it is possible to calculate path loss. 27 | var txPowerLevel: Double? { get } 28 | 29 | /// Service-specific advertisement data. 30 | var serviceData: [BluetoothUUID: Data]? { get } 31 | 32 | /// An array of service UUIDs 33 | var serviceUUIDs: [BluetoothUUID]? { get } 34 | 35 | /// An array of one or more `BluetoothUUID`, representing Service UUIDs. 36 | var solicitedServiceUUIDs: [BluetoothUUID]? { get } 37 | } 38 | 39 | #if canImport(BluetoothGAP) 40 | 41 | // MARK: - LowEnergyAdvertisingData 42 | 43 | extension LowEnergyAdvertisingData: AdvertisementData { 44 | 45 | public typealias Data = Self 46 | 47 | /// The local name of a peripheral. 48 | public var localName: String? { 49 | 50 | if let decoded = try? GAPDataDecoder.decode(GAPCompleteLocalName.self, from: self) { 51 | return decoded.name 52 | } else if let decoded = try? GAPDataDecoder.decode(GAPShortLocalName.self, from: self) { 53 | return decoded.name 54 | } else { 55 | return nil 56 | } 57 | } 58 | 59 | /// The Manufacturer data of a peripheral. 60 | public var manufacturerData: GAPManufacturerSpecificData? { 61 | 62 | guard let value = try? GAPDataDecoder.decode(GAPManufacturerSpecificData.self, from: self) 63 | else { return nil } 64 | 65 | return ManufacturerSpecificData( 66 | companyIdentifier: value.companyIdentifier, 67 | additionalData: value.additionalData 68 | ) 69 | } 70 | 71 | /// Service-specific advertisement data. 72 | public var serviceData: [BluetoothUUID: Self]? { 73 | 74 | var serviceData = [BluetoothUUID: Self](minimumCapacity: 3) 75 | 76 | if let value = try? GAPDataDecoder.decode(GAPServiceData16BitUUID.self, from: self) { 77 | serviceData[.bit16(value.uuid)] = value.serviceData 78 | } 79 | if let value = try? GAPDataDecoder.decode(GAPServiceData32BitUUID.self, from: self) { 80 | serviceData[.bit32(value.uuid)] = value.serviceData 81 | } 82 | if let value = try? GAPDataDecoder.decode(GAPServiceData128BitUUID.self, from: self) { 83 | serviceData[.bit128(UInt128(uuid: value.uuid))] = value.serviceData 84 | } 85 | 86 | guard serviceData.isEmpty == false 87 | else { return nil } 88 | 89 | return serviceData 90 | } 91 | 92 | /// An array of service UUIDs 93 | public var serviceUUIDs: [BluetoothUUID]? { 94 | 95 | var uuids = [BluetoothUUID]() 96 | uuids.reserveCapacity(2) 97 | 98 | if let value = try? GAPDataDecoder.decode(GAPCompleteListOf16BitServiceClassUUIDs.self, from: self) { 99 | uuids += value.uuids.map { .bit16($0) } 100 | } 101 | if let value = try? GAPDataDecoder.decode(GAPIncompleteListOf16BitServiceClassUUIDs.self, from: self) { 102 | uuids += value.uuids.map { .bit16($0) } 103 | } 104 | if let value = try? GAPDataDecoder.decode(GAPIncompleteListOf32BitServiceClassUUIDs.self, from: self) { 105 | uuids += value.uuids.map { .bit32($0) } 106 | } 107 | if let value = try? GAPDataDecoder.decode(GAPIncompleteListOf32BitServiceClassUUIDs.self, from: self) { 108 | uuids += value.uuids.map { .bit32($0) } 109 | } 110 | if let value = try? GAPDataDecoder.decode(GAPCompleteListOf128BitServiceClassUUIDs.self, from: self) { 111 | uuids += value.uuids.map { .init(uuid: $0) } 112 | } 113 | if let value = try? GAPDataDecoder.decode(GAPIncompleteListOf128BitServiceClassUUIDs.self, from: self) { 114 | uuids += value.uuids.map { .init(uuid: $0) } 115 | } 116 | 117 | guard uuids.isEmpty == false 118 | else { return nil } 119 | 120 | return uuids 121 | } 122 | 123 | /// This value is available if the broadcaster (peripheral) provides its Tx power level in its advertising packet. 124 | /// Using the RSSI value and the Tx power level, it is possible to calculate path loss. 125 | public var txPowerLevel: Double? { 126 | 127 | guard let value = try? GAPDataDecoder.decode(GAPTxPowerLevel.self, from: self) 128 | else { return nil } 129 | 130 | return Double(value.powerLevel) 131 | } 132 | 133 | /// An array of one or more `BluetoothUUID`, representing Service UUIDs. 134 | public var solicitedServiceUUIDs: [BluetoothUUID]? { 135 | 136 | var uuids = [BluetoothUUID]() 137 | uuids.reserveCapacity(2) 138 | 139 | if let value = try? GAPDataDecoder.decode(GAPListOf16BitServiceSolicitationUUIDs.self, from: self) { 140 | uuids += value.uuids.map { .bit16($0) } 141 | } 142 | if let value = try? GAPDataDecoder.decode(GAPListOf32BitServiceSolicitationUUIDs.self, from: self) { 143 | uuids += value.uuids.map { .bit32($0) } 144 | } 145 | if let value = try? GAPDataDecoder.decode(GAPListOf128BitServiceSolicitationUUIDs.self, from: self) { 146 | uuids += value.uuids.map { .init(uuid: $0) } 147 | } 148 | 149 | guard uuids.isEmpty == false 150 | else { return nil } 151 | 152 | return uuids 153 | } 154 | } 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /Tests/GATTTests/TestL2CAPSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L2CAPSocket.swift 3 | // BluetoothTests 4 | // 5 | // Created by Alsey Coleman Miller on 3/30/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if !os(WASI) 10 | import Foundation 11 | import Bluetooth 12 | import GATT 13 | 14 | internal final class TestL2CAPServer: L2CAPServer { 15 | 16 | typealias Error = POSIXError 17 | 18 | enum Cache { 19 | 20 | static let lock = NSLock() 21 | 22 | nonisolated(unsafe) static var pendingClients = [BluetoothAddress: [TestL2CAPSocket]]() 23 | 24 | static func queue(client socket: TestL2CAPSocket, server: BluetoothAddress) { 25 | lock.lock() 26 | defer { lock.unlock() } 27 | pendingClients[server, default: []].append(socket) 28 | } 29 | 30 | static func dequeue(server: BluetoothAddress) -> TestL2CAPSocket? { 31 | lock.lock() 32 | defer { lock.unlock() } 33 | guard let socket = pendingClients[server]?.first else { 34 | return nil 35 | } 36 | pendingClients[server]?.removeFirst() 37 | return socket 38 | } 39 | 40 | static func canAccept(server: BluetoothAddress) -> Bool { 41 | lock.lock() 42 | defer { lock.unlock() } 43 | return pendingClients[server, default: []].isEmpty == false 44 | } 45 | } 46 | 47 | let name: String 48 | 49 | let address: BluetoothAddress 50 | 51 | var status: L2CAPSocketStatus { 52 | .init( 53 | send: false, 54 | recieve: false, 55 | accept: Cache.canAccept(server: address) 56 | ) 57 | } 58 | 59 | init(name: String, address: BluetoothAddress) { 60 | self.name = name 61 | self.address = address 62 | } 63 | 64 | deinit { 65 | close() 66 | } 67 | 68 | static func lowEnergyServer( 69 | address: BluetoothAddress, 70 | isRandom: Bool, 71 | backlog: Int 72 | ) throws(POSIXError) -> TestL2CAPServer { 73 | return TestL2CAPServer( 74 | name: "Server", 75 | address: address 76 | ) 77 | } 78 | 79 | func accept() throws(POSIXError) -> TestL2CAPSocket { 80 | // dequeue socket 81 | guard let client = Cache.dequeue(server: address) else { 82 | throw POSIXError(.EAGAIN) 83 | } 84 | let newConnection = TestL2CAPSocket( 85 | address: client.address, 86 | destination: self.address, 87 | name: "Server connection" 88 | ) 89 | // connect sockets 90 | newConnection.connect(to: client) 91 | client.connect(to: newConnection) 92 | return newConnection 93 | } 94 | 95 | func close() { 96 | 97 | } 98 | } 99 | 100 | /// Test L2CAP socket 101 | internal final class TestL2CAPSocket: L2CAPConnection { 102 | 103 | typealias Data = Foundation.Data 104 | 105 | typealias Error = POSIXError 106 | 107 | static func lowEnergyClient( 108 | address: BluetoothAddress, 109 | destination: BluetoothAddress, 110 | isRandom: Bool 111 | ) throws(POSIXError) -> TestL2CAPSocket { 112 | let socket = TestL2CAPSocket( 113 | address: address, 114 | destination: destination, 115 | name: "Client" 116 | ) 117 | TestL2CAPServer.Cache.queue(client: socket, server: destination) 118 | return socket 119 | } 120 | 121 | // MARK: - Properties 122 | 123 | let name: String 124 | 125 | let address: BluetoothAddress 126 | 127 | let destination: Bluetooth.BluetoothAddress 128 | 129 | var status: L2CAPSocketStatus { 130 | .init( 131 | send: target != nil, 132 | recieve: target != nil && receivedData.isEmpty == false, 133 | accept: false, 134 | error: nil 135 | ) 136 | } 137 | 138 | func securityLevel() throws(POSIXError) -> Bluetooth.SecurityLevel { 139 | _securityLevel 140 | } 141 | 142 | private var _securityLevel: Bluetooth.SecurityLevel = .sdp 143 | 144 | /// Attempts to change the socket's security level. 145 | func setSecurityLevel(_ securityLevel: SecurityLevel) throws(POSIXError) { 146 | _securityLevel = securityLevel 147 | } 148 | 149 | /// Target socket. 150 | private weak var target: TestL2CAPSocket? 151 | 152 | fileprivate(set) var receivedData = [Foundation.Data]() 153 | 154 | private(set) var cache = [Foundation.Data]() 155 | 156 | // MARK: - Initialization 157 | 158 | init( 159 | address: BluetoothAddress = .zero, 160 | destination: Bluetooth.BluetoothAddress, 161 | name: String 162 | ) { 163 | self.address = address 164 | self.destination = destination 165 | self.name = name 166 | } 167 | 168 | deinit { 169 | close() 170 | } 171 | 172 | // MARK: - Methods 173 | 174 | func close() { 175 | target = nil 176 | target?.target = nil 177 | } 178 | 179 | /// Write to the socket. 180 | func send(_ data: Data) throws(POSIXError) { 181 | 182 | print("L2CAP Socket: \(name) will send \(data.count) bytes") 183 | 184 | guard let target = self.target 185 | else { throw POSIXError(.ECONNRESET) } 186 | 187 | target.receive(data) 188 | } 189 | 190 | /// Reads from the socket. 191 | func receive(_ bufferSize: Int) throws(POSIXError) -> Data { 192 | 193 | print("L2CAP Socket: \(name) will read \(bufferSize) bytes") 194 | 195 | guard self.target != nil 196 | else { throw POSIXError(.ECONNRESET) } 197 | 198 | guard self.receivedData.isEmpty == false else { 199 | throw POSIXError(.EAGAIN) 200 | } 201 | 202 | let data = self.receivedData.removeFirst() 203 | cache.append(data) 204 | return data 205 | } 206 | 207 | fileprivate func receive(_ data: Data) { 208 | receivedData.append(data) 209 | print("L2CAP Socket: \(name) received \([UInt8](data))") 210 | } 211 | 212 | internal func connect(to socket: TestL2CAPSocket) { 213 | self.target = socket 214 | } 215 | } 216 | 217 | #endif 218 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/DarwinDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarwinDescriptor.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 6/13/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if canImport(CoreBluetooth) 10 | import Foundation 11 | import CoreBluetooth 12 | import Bluetooth 13 | 14 | /// Darwin Characteristic Descriptor 15 | /// 16 | /// [Documentation](https://developer.apple.com/documentation/corebluetooth/cbuuid/characteristic_descriptors) 17 | internal enum DarwinDescriptor { 18 | 19 | /// Characteristic Extended Properties 20 | /// 21 | /// The corresponding value for this descriptor is an `NSNumber` object. 22 | case extendedProperties(NSNumber) 23 | 24 | /// Characteristic User Description Descriptor 25 | /// 26 | /// The corresponding value for this descriptor is an `NSString` object. 27 | case userDescription(NSString) 28 | 29 | /// Characteristic Format Descriptor 30 | /// 31 | /// The corresponding value for this descriptor is an `NSData` object 32 | case format(NSData) 33 | 34 | /// Characteristic Aggregate Format 35 | /// 36 | case aggregateFormat(NSData) 37 | 38 | /// Client Characteristic Configuration 39 | /// 40 | /// The corresponding value for this descriptor is an `NSNumber` object. 41 | case clientConfiguration(NSNumber) 42 | 43 | /// Server Characteristic Configuration 44 | /// 45 | /// The corresponding value for this descriptor is an `NSNumber` object. 46 | case serverConfiguration(NSNumber) 47 | 48 | } 49 | 50 | extension DarwinDescriptor { 51 | 52 | init?(_ descriptor: CBDescriptor) { 53 | let uuid = BluetoothUUID(descriptor.uuid) 54 | switch uuid { 55 | case BluetoothUUID.Descriptor.characteristicUserDescription: 56 | guard let userDescription = descriptor.value as? NSString 57 | else { return nil } 58 | self = .userDescription(userDescription) 59 | case BluetoothUUID.Descriptor.characteristicPresentationFormat: 60 | guard let data = descriptor.value as? NSData 61 | else { return nil } 62 | self = .format(data) 63 | case BluetoothUUID.Descriptor.characteristicExtendedProperties: 64 | guard let data = descriptor.value as? NSNumber 65 | else { return nil } 66 | self = .extendedProperties(data) 67 | case BluetoothUUID.Descriptor.characteristicAggregateFormat: 68 | guard let data = descriptor.value as? NSData 69 | else { return nil } 70 | self = .aggregateFormat(data) 71 | case BluetoothUUID.Descriptor.clientCharacteristicConfiguration: 72 | guard let data = descriptor.value as? NSNumber 73 | else { return nil } 74 | self = .clientConfiguration(data) 75 | case BluetoothUUID.Descriptor.serverCharacteristicConfiguration: 76 | guard let data = descriptor.value as? NSNumber 77 | else { return nil } 78 | self = .serverConfiguration(data) 79 | default: 80 | return nil 81 | } 82 | assert(self.uuid == uuid) 83 | } 84 | 85 | var uuid: BluetoothUUID { 86 | switch self { 87 | case .format: return BluetoothUUID.Descriptor.characteristicPresentationFormat 88 | case .userDescription: return BluetoothUUID.Descriptor.characteristicUserDescription 89 | case .extendedProperties: return BluetoothUUID.Descriptor.characteristicExtendedProperties 90 | case .aggregateFormat: return BluetoothUUID.Descriptor.characteristicAggregateFormat 91 | case .clientConfiguration: return BluetoothUUID.Descriptor.clientCharacteristicConfiguration 92 | case .serverConfiguration: return BluetoothUUID.Descriptor.serverCharacteristicConfiguration 93 | } 94 | } 95 | 96 | var value: AnyObject { 97 | switch self { 98 | case let .format(value): return value 99 | case let .userDescription(value): return value 100 | case let .aggregateFormat(value): return value 101 | case let .extendedProperties(value): return value 102 | case let .clientConfiguration(value): return value 103 | case let .serverConfiguration(value): return value 104 | } 105 | } 106 | 107 | var data: Data { 108 | switch self { 109 | case let .userDescription(value): 110 | return Data((value as String).utf8) 111 | case let .format(value): 112 | return value as Data 113 | case let .aggregateFormat(value): 114 | return value as Data 115 | case let .extendedProperties(value): 116 | return Data([value.uint8Value]) 117 | case let .clientConfiguration(value): 118 | let bytes = value.uint16Value.littleEndian.bytes 119 | return Data([bytes.0, bytes.1]) 120 | case let .serverConfiguration(value): 121 | let bytes = value.uint16Value.littleEndian.bytes 122 | return Data([bytes.0, bytes.1]) 123 | } 124 | } 125 | } 126 | 127 | #if (os(macOS) || os(iOS)) && canImport(BluetoothGATT) 128 | internal extension CBMutableDescriptor { 129 | 130 | /// Only the characteristic user description descriptor and the characteristic format descriptor 131 | /// are supported for descriptors for use in local Peripherals. 132 | static var supportedUUID: Set { 133 | return [BluetoothUUID.Descriptor.characteristicUserDescription, BluetoothUUID.Descriptor.characteristicPresentationFormat] 134 | } 135 | 136 | convenience init?(_ descriptor: DarwinDescriptor) { 137 | guard Self.supportedUUID.contains(descriptor.uuid) else { return nil } 138 | self.init(type: CBUUID(descriptor.uuid), value: descriptor.value) 139 | } 140 | 141 | convenience init?(uuid: BluetoothUUID, data: Data) { 142 | let descriptor: DarwinDescriptor 143 | switch uuid { 144 | case BluetoothUUID.Descriptor.characteristicUserDescription: 145 | guard let userDescription = String(data: data, encoding: .utf8) 146 | else { return nil } 147 | descriptor = .userDescription(userDescription as NSString) 148 | case BluetoothUUID.Descriptor.characteristicPresentationFormat: 149 | descriptor = .format(data as NSData) 150 | default: 151 | assert(Self.supportedUUID.contains(uuid) == false) 152 | return nil 153 | } 154 | self.init(descriptor) 155 | } 156 | } 157 | #endif 158 | #endif 159 | -------------------------------------------------------------------------------- /Sources/GATT/GATTCentral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GATTCentral.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/18/18. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | #endif 11 | #if canImport(BluetoothGATT) && canImport(BluetoothHCI) 12 | @_exported import Bluetooth 13 | @_exported import BluetoothGATT 14 | @_exported import BluetoothHCI 15 | 16 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 17 | public final class GATTCentral : CentralManager, @unchecked Sendable where Socket: Sendable { 18 | 19 | public typealias Peripheral = GATT.Peripheral 20 | 21 | public typealias Data = Socket.Data 22 | 23 | public typealias Options = GATTCentralOptions 24 | 25 | // MARK: - Properties 26 | 27 | public typealias Advertisement = LowEnergyAdvertisingData 28 | 29 | public typealias AttributeID = UInt16 30 | 31 | public var log: (@Sendable (String) -> ())? 32 | 33 | public let hostController: HostController 34 | 35 | public let options: Options 36 | 37 | /// Currently scanned devices, or restored devices. 38 | public var peripherals: [Peripheral: Bool] { 39 | get async { 40 | await storage.peripherals 41 | } 42 | } 43 | 44 | internal let storage = Storage() 45 | 46 | // MARK: - Initialization 47 | 48 | public init( 49 | hostController: HostController, 50 | options: Options = Options(), 51 | socket: Socket.Type 52 | ) { 53 | self.hostController = hostController 54 | self.options = options 55 | } 56 | 57 | // MARK: - Methods 58 | 59 | /// Scans for peripherals that are advertising services. 60 | public func scan( 61 | filterDuplicates: Bool 62 | ) async throws -> AsyncCentralScan { 63 | let scanParameters = HCILESetScanParameters( 64 | type: .active, 65 | interval: LowEnergyScanTimeInterval(rawValue: 0x01E0)!, 66 | window: LowEnergyScanTimeInterval(rawValue: 0x0030)!, 67 | addressType: .public, 68 | filterPolicy: .accept 69 | ) 70 | return try await scan(filterDuplicates: filterDuplicates, parameters: scanParameters) 71 | } 72 | 73 | /// Scans for peripherals that are advertising services. 74 | public func scan( 75 | filterDuplicates: Bool, 76 | parameters: HCILESetScanParameters 77 | ) async throws -> AsyncCentralScan { 78 | self.log?("Scanning...") 79 | let stream = try await self.hostController.lowEnergyScan( 80 | filterDuplicates: filterDuplicates, 81 | parameters: parameters 82 | ) 83 | return AsyncCentralScan { [unowned self] continuation in 84 | // start scanning 85 | for try await report in stream { 86 | let scanData = await self.storage.found(report) 87 | continuation(scanData) 88 | } 89 | } 90 | } 91 | 92 | public func connect(to peripheral: Peripheral) async throws { 93 | // get scan data (Bluetooth address) for new connection 94 | guard let (scanData, report) = await self.storage.scanData[peripheral] 95 | else { throw CentralError.unknownPeripheral } 96 | // log 97 | self.log(scanData.peripheral, "Open connection (\(report.addressType))") 98 | // load cache device address 99 | let localAddress = try await storage.readAddress(hostController) 100 | // open socket 101 | let socket = try Socket.lowEnergyClient( 102 | address: localAddress, 103 | destination: report 104 | ) 105 | let connection = await GATTClientConnection( 106 | peripheral: peripheral, 107 | socket: socket, 108 | maximumTransmissionUnit: self.options.maximumTransmissionUnit, 109 | log: { [weak self] in 110 | self?.log(peripheral, $0) 111 | } 112 | ) 113 | // store connection 114 | await self.storage.didConnect(connection, socket) 115 | Task.detached { [weak self, weak connection] in 116 | do { 117 | while let connection { 118 | try await Task.sleep(nanoseconds: 10_000) 119 | try await connection.run() 120 | } 121 | } 122 | catch { 123 | self?.log(peripheral, error.localizedDescription) 124 | } 125 | await self?.storage.removeConnection(peripheral) 126 | } 127 | } 128 | 129 | public func disconnect(_ peripheral: Peripheral) async { 130 | if let (_, socket) = await storage.connections[peripheral] { 131 | socket.close() 132 | } 133 | await storage.removeConnection(peripheral) 134 | } 135 | 136 | public func disconnectAll() async { 137 | for (_, socket) in await storage.connections.values { 138 | socket.close() 139 | } 140 | await storage.removeAllConnections() 141 | } 142 | 143 | public func discoverServices( 144 | _ services: Set = [], 145 | for peripheral: Peripheral 146 | ) async throws -> [Service] { 147 | return try await connection(for: peripheral) 148 | .discoverServices(services) 149 | } 150 | 151 | /// Discover Characteristics for service 152 | public func discoverCharacteristics( 153 | _ characteristics: Set = [], 154 | for service: Service 155 | ) async throws -> [Characteristic] { 156 | return try await connection(for: service.peripheral) 157 | .discoverCharacteristics(characteristics, for: service) 158 | } 159 | 160 | /// Read Characteristic Value 161 | public func readValue( 162 | for characteristic: Characteristic 163 | ) async throws -> Data { 164 | return try await connection(for: characteristic.peripheral) 165 | .readValue(for: characteristic) 166 | } 167 | 168 | /// Write Characteristic Value 169 | public func writeValue( 170 | _ data: Data, 171 | for characteristic: Characteristic, 172 | withResponse: Bool = true 173 | ) async throws { 174 | try await connection(for: characteristic.peripheral) 175 | .writeValue(data, for: characteristic, withResponse: withResponse) 176 | } 177 | 178 | /// Start Notifications 179 | public func notify( 180 | for characteristic: Characteristic 181 | ) -> AsyncCentralNotifications { 182 | return AsyncCentralNotifications(onTermination: { 183 | Task(priority: .userInitiated) { [weak self] in 184 | guard let self = self else { return } 185 | do { 186 | // start notifications 187 | try await self.connection(for: characteristic.peripheral) 188 | .notify(characteristic, notification: .none) 189 | } 190 | catch { 191 | self.log?("Unable to stop notifications for \(characteristic.uuid)") 192 | } 193 | } 194 | }) { continuation in 195 | Task(priority: .userInitiated) { 196 | do { 197 | // start notifications 198 | try await connection(for: characteristic.peripheral) 199 | .notify(characteristic) { continuation.yield($0) } 200 | } 201 | catch { 202 | continuation.finish(throwing: error) 203 | } 204 | } 205 | } 206 | } 207 | 208 | public func discoverDescriptors(for characteristic: Characteristic) async throws -> [Descriptor] { 209 | try await connection(for: characteristic.peripheral) 210 | .discoverDescriptors(for: characteristic) 211 | } 212 | 213 | public func readValue(for descriptor: Descriptor) async throws -> Data { 214 | try await connection(for: descriptor.peripheral) 215 | .readValue(for: descriptor) 216 | } 217 | 218 | public func writeValue(_ data: Data, for descriptor: Descriptor) async throws { 219 | try await connection(for: descriptor.peripheral) 220 | .writeValue(data, for: descriptor) 221 | } 222 | 223 | /// Read MTU 224 | public func maximumTransmissionUnit(for peripheral: Peripheral) async throws -> MaximumTransmissionUnit { 225 | return try await connection(for: peripheral) 226 | .client.maximumTransmissionUnit 227 | } 228 | 229 | // Read RSSI 230 | public func rssi(for peripheral: Peripheral) async throws -> RSSI { 231 | RSSI(rawValue: +20)! 232 | } 233 | 234 | // MARK: - Private Methods 235 | 236 | private func connection(for peripheral: Peripheral) async throws -> GATTClientConnection { 237 | 238 | guard await storage.scanData.keys.contains(peripheral) 239 | else { throw CentralError.unknownPeripheral } 240 | 241 | guard let (connection, _) = await storage.connections[peripheral] 242 | else { throw CentralError.disconnected } 243 | 244 | return connection 245 | } 246 | 247 | private func log(_ peripheral: Peripheral, _ message: String) { 248 | log?("[\(peripheral)]: " + message) 249 | } 250 | } 251 | 252 | // MARK: - Supporting Types 253 | 254 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 255 | internal extension GATTCentral { 256 | 257 | actor Storage { 258 | 259 | var address: BluetoothAddress? 260 | 261 | var scanData = [Peripheral: (ScanData, HCILEAdvertisingReport.Report)]() 262 | 263 | var connections = [Peripheral: (connection: GATTClientConnection, socket: Socket)](minimumCapacity: 2) 264 | 265 | var peripherals: [Peripheral: Bool] { 266 | get async { 267 | let peripherals = scanData.keys 268 | let connections = connections.keys 269 | var result = [Peripheral: Bool]() 270 | result.reserveCapacity(peripherals.count) 271 | return peripherals.reduce(into: result, { $0[$1] = connections.contains($1) }) 272 | } 273 | } 274 | 275 | func found(_ report: HCILEAdvertisingReport.Report) -> ScanData { 276 | let peripheral = Peripheral(id: report.address) 277 | let scanData = ScanData( 278 | peripheral: peripheral, 279 | date: Date(), 280 | rssi: Double(report.rssi?.rawValue ?? 0), 281 | advertisementData: report.responseData, 282 | isConnectable: report.event.isConnectable 283 | ) 284 | self.scanData[peripheral] = (scanData, report) 285 | return scanData 286 | } 287 | 288 | func readAddress(_ hostController: HostController) async throws -> BluetoothAddress { 289 | if let cachedAddress = self.address { 290 | return cachedAddress 291 | } else { 292 | let address = try await hostController.readDeviceAddress() 293 | self.address = address 294 | return address 295 | } 296 | } 297 | 298 | func didConnect(_ connection: GATTClientConnection, _ socket: Socket) { 299 | self.connections[connection.peripheral] = (connection, socket) 300 | } 301 | 302 | func removeConnection(_ peripheral: Peripheral) async { 303 | self.connections[peripheral] = nil 304 | } 305 | 306 | func removeAllConnections() async { 307 | self.connections.removeAll(keepingCapacity: true) 308 | } 309 | } 310 | } 311 | 312 | public struct GATTCentralOptions { 313 | 314 | public let maximumTransmissionUnit: ATTMaximumTransmissionUnit 315 | 316 | public init(maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default) { 317 | self.maximumTransmissionUnit = maximumTransmissionUnit 318 | } 319 | } 320 | 321 | #endif 322 | -------------------------------------------------------------------------------- /Sources/GATT/GATTClientConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GATTClientConnection.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/18/18. 6 | // 7 | 8 | #if canImport(BluetoothGATT) 9 | import Bluetooth 10 | import BluetoothGATT 11 | 12 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 13 | internal actor GATTClientConnection where Socket: Sendable { 14 | 15 | typealias Data = Socket.Data 16 | 17 | // MARK: - Properties 18 | 19 | let peripheral: Peripheral 20 | 21 | let client: GATTClient 22 | 23 | private var cache = Cache() 24 | 25 | var maximumUpdateValueLength: Int { 26 | get async { 27 | // ATT_MTU-3 28 | return await Int(client.maximumTransmissionUnit.rawValue) - 3 29 | } 30 | } 31 | 32 | // MARK: - Initialization 33 | 34 | init( 35 | peripheral: Peripheral, 36 | socket: Socket, 37 | maximumTransmissionUnit: ATTMaximumTransmissionUnit, 38 | log: (@Sendable (String) -> ())? = nil 39 | ) async { 40 | self.peripheral = peripheral 41 | self.client = await GATTClient( 42 | socket: socket, 43 | maximumTransmissionUnit: maximumTransmissionUnit, 44 | log: log 45 | ) 46 | } 47 | 48 | // MARK: - Methods 49 | 50 | public func run() async throws { 51 | try await client.run() 52 | } 53 | 54 | public func discoverServices( 55 | _ services: Set 56 | ) async throws -> [Service] { 57 | let foundServices = try await client.discoverAllPrimaryServices() 58 | cache.insert(foundServices) 59 | return cache.services.map { 60 | Service( 61 | id: $0.key, 62 | uuid: $0.value.attribute.uuid, 63 | peripheral: peripheral, 64 | isPrimary: $0.value.attribute.isPrimary 65 | ) 66 | } 67 | } 68 | 69 | public func discoverCharacteristics( 70 | _ characteristics: Set, 71 | for service: Service 72 | ) async throws -> [Characteristic] { 73 | 74 | assert(service.peripheral == peripheral) 75 | 76 | // get service 77 | guard let gattService = cache.service(for: service.id)?.attribute 78 | else { throw CentralError.invalidAttribute(service.uuid) } 79 | 80 | // GATT request 81 | let foundCharacteristics = try await self.client.discoverAllCharacteristics(of: gattService) 82 | 83 | // store in cache 84 | cache.insert(foundCharacteristics, for: service.id) 85 | return cache.service(for: service.id)?.characteristics.map { 86 | Characteristic(id: $0.key, 87 | uuid: $0.value.attribute.uuid, 88 | peripheral: peripheral, 89 | properties: $0.value.attribute.properties) 90 | } ?? [] 91 | } 92 | 93 | public func readValue(for characteristic: Characteristic) async throws -> Data { 94 | 95 | assert(characteristic.peripheral == peripheral) 96 | 97 | // GATT characteristic 98 | guard let (_ , gattCharacteristic) = cache.characteristic(for: characteristic.id) 99 | else { throw CentralError.invalidAttribute(characteristic.uuid) } 100 | 101 | // GATT request 102 | return try await client.readCharacteristic(gattCharacteristic.attribute) 103 | } 104 | 105 | public func writeValue( 106 | _ data: Data, 107 | for characteristic: Characteristic, 108 | withResponse: Bool 109 | ) async throws { 110 | 111 | assert(characteristic.peripheral == peripheral) 112 | 113 | // GATT characteristic 114 | guard let (_ , gattCharacteristic) = cache.characteristic(for: characteristic.id) 115 | else { throw CentralError.invalidAttribute(characteristic.uuid) } 116 | 117 | // GATT request 118 | try await client.writeCharacteristic(gattCharacteristic.attribute, data: data, withResponse: withResponse) 119 | } 120 | 121 | public func discoverDescriptors( 122 | for characteristic: Characteristic 123 | ) async throws -> [Descriptor] { 124 | 125 | assert(characteristic.peripheral == peripheral) 126 | 127 | // GATT characteristic 128 | guard let (gattService, gattCharacteristic) = cache.characteristic(for: characteristic.id) 129 | else { throw CentralError.invalidAttribute(characteristic.uuid) } 130 | 131 | let service = ( 132 | declaration: gattService.attribute, 133 | characteristics: gattService.characteristics 134 | .values 135 | .lazy 136 | .sorted { $0.attribute.handle.declaration < $1.attribute.handle.declaration } 137 | .map { $0.attribute } 138 | ) 139 | 140 | // GATT request 141 | let foundDescriptors = try await client.discoverDescriptors(of: gattCharacteristic.attribute, service: service) 142 | 143 | // update cache 144 | cache.insert(foundDescriptors, for: characteristic.id) 145 | return foundDescriptors.map { 146 | Descriptor( 147 | id: $0.handle, 148 | uuid: $0.uuid, 149 | peripheral: characteristic.peripheral 150 | ) 151 | } 152 | } 153 | 154 | public func readValue(for descriptor: Descriptor) async throws -> Data { 155 | assert(descriptor.peripheral == peripheral) 156 | guard let (_, _, gattDescriptor) = cache.descriptor(for: descriptor.id) else { 157 | throw CentralError.invalidAttribute(descriptor.uuid) 158 | } 159 | return try await client.readDescriptor(gattDescriptor.attribute) 160 | } 161 | 162 | public func writeValue(_ data: Data, for descriptor: Descriptor) async throws { 163 | assert(descriptor.peripheral == peripheral) 164 | guard let (_, _, gattDescriptor) = cache.descriptor(for: descriptor.id) else { 165 | throw CentralError.invalidAttribute(descriptor.uuid) 166 | } 167 | try await client.writeDescriptor(gattDescriptor.attribute, data: data) 168 | } 169 | 170 | public func notify( 171 | _ characteristic: Characteristic, 172 | notification: (GATTClient.Notification)? 173 | ) async throws { 174 | 175 | assert(characteristic.peripheral == peripheral) 176 | 177 | // GATT characteristic 178 | guard let (_ , gattCharacteristic) = cache.characteristic(for: characteristic.id) 179 | else { throw CentralError.invalidAttribute(characteristic.uuid) } 180 | 181 | // Gatt Descriptors 182 | let descriptors: [GATTClient.Descriptor] 183 | if gattCharacteristic.descriptors.values.contains(where: { $0.attribute.uuid == BluetoothUUID.Descriptor.clientCharacteristicConfiguration }) { 184 | descriptors = Array(gattCharacteristic.descriptors.values.map { $0.attribute }) 185 | } else { 186 | // fetch descriptors 187 | let _ = try await self.discoverDescriptors(for: characteristic) 188 | // get updated cache 189 | if let cache = cache.characteristic(for: characteristic.id)?.1.descriptors.values.map({ $0.attribute }) { 190 | descriptors = Array(cache) 191 | } else { 192 | descriptors = [] 193 | } 194 | } 195 | 196 | let notify: GATTClient.Notification? 197 | let indicate: GATTClient.Notification? 198 | 199 | /** 200 | If the specified characteristic is configured to allow both notifications and indications, calling this method enables notifications only. You can disable notifications and indications for a characteristic’s value by calling this method with the enabled parameter set to false. 201 | */ 202 | if gattCharacteristic.attribute.properties.contains(.notify) { 203 | notify = notification 204 | indicate = nil 205 | } else if gattCharacteristic.attribute.properties.contains(.indicate) { 206 | notify = nil 207 | indicate = notification 208 | } else { 209 | notify = nil 210 | indicate = nil 211 | assertionFailure("Cannot enable notification or indication for characteristic \(characteristic.uuid)") 212 | return 213 | } 214 | 215 | // GATT request 216 | try await client.clientCharacteristicConfiguration( 217 | gattCharacteristic.attribute, 218 | notification: notify, 219 | indication: indicate, 220 | descriptors: descriptors 221 | ) 222 | } 223 | } 224 | 225 | // MARK: - Supporting Types 226 | 227 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 228 | internal extension GATTClientConnection { 229 | 230 | struct Cache { 231 | 232 | fileprivate init() { } 233 | 234 | private(set) var services = [UInt16: Cache.Service](minimumCapacity: 1) 235 | } 236 | } 237 | 238 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 239 | internal extension GATTClientConnection.Cache { 240 | 241 | func service(for identifier: UInt16) -> Service? { 242 | return services[identifier] 243 | } 244 | 245 | func characteristic(for identifier: UInt16) -> (Service, Characteristic)? { 246 | 247 | for service in services.values { 248 | guard let characteristic = service.characteristics[identifier] 249 | else { continue } 250 | return (service, characteristic) 251 | } 252 | 253 | return nil 254 | } 255 | 256 | func descriptor(for identifier: UInt16) -> (Service, Characteristic, Descriptor)? { 257 | 258 | for service in services.values { 259 | for characteristic in service.characteristics.values { 260 | guard let descriptor = characteristic.descriptors[identifier] 261 | else { continue } 262 | return (service, characteristic, descriptor) 263 | } 264 | } 265 | 266 | return nil 267 | } 268 | 269 | mutating func insert(_ newValues: [GATTClient.Service]) { 270 | services.removeAll(keepingCapacity: true) 271 | newValues.forEach { 272 | services[$0.handle] = Service(attribute: $0, characteristics: [:]) 273 | } 274 | } 275 | 276 | mutating func insert( 277 | _ newValues: [GATTClient.Characteristic], 278 | for service: UInt16 279 | ) { 280 | 281 | // remove old values 282 | services[service]?.characteristics.removeAll(keepingCapacity: true) 283 | // insert new values 284 | newValues.forEach { 285 | services[service]?.characteristics[$0.handle.declaration] = Characteristic(attribute: $0, notification: nil, descriptors: [:]) 286 | } 287 | } 288 | 289 | mutating func insert(_ newValues: [GATTClient.Descriptor], 290 | for characteristic: UInt16) { 291 | 292 | var descriptorsCache = [UInt16: Descriptor](minimumCapacity: newValues.count) 293 | newValues.forEach { 294 | descriptorsCache[$0.handle] = Descriptor(attribute: $0) 295 | } 296 | for (serviceIdentifier, service) in services { 297 | guard let _ = service.characteristics[characteristic] 298 | else { continue } 299 | services[serviceIdentifier]?.characteristics[characteristic]?.descriptors = descriptorsCache 300 | } 301 | } 302 | } 303 | 304 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 305 | internal extension GATTClientConnection.Cache { 306 | 307 | struct Service { 308 | 309 | let attribute: GATTClient.Service 310 | 311 | var characteristics = [UInt16: Characteristic]() 312 | } 313 | 314 | struct Characteristic { 315 | 316 | let attribute: GATTClient.Characteristic 317 | 318 | var notification: GATTClient.Notification? 319 | 320 | var descriptors = [UInt16: Descriptor]() 321 | } 322 | 323 | struct Descriptor { 324 | 325 | let attribute: GATTClient.Descriptor 326 | } 327 | } 328 | 329 | #endif 330 | 331 | -------------------------------------------------------------------------------- /Sources/GATT/GATTPeripheral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GATTPeripheral.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/18/18. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | #endif 11 | #if canImport(BluetoothGATT) && canImport(BluetoothHCI) 12 | @_exported import Bluetooth 13 | @_exported import BluetoothGATT 14 | @_exported import BluetoothHCI 15 | 16 | /// GATT Peripheral Manager 17 | public final class GATTPeripheral : PeripheralManager, @unchecked Sendable where Socket.Error == Socket.Connection.Error { 18 | 19 | /// Central Peer 20 | public typealias Central = GATT.Central 21 | 22 | /// Peripheral Options 23 | public typealias Options = GATTPeripheralOptions 24 | 25 | /// Peripheral Advertising Options 26 | public typealias AdvertisingOptions = GATTPeripheralAdvertisingOptions 27 | 28 | public typealias Data = Socket.Connection.Data 29 | 30 | // MARK: - Properties 31 | 32 | public let hostController: HostController 33 | 34 | public let options: Options 35 | 36 | /// Logging 37 | public var log: (@Sendable (String) -> ())? { 38 | get { 39 | storage.log 40 | } 41 | set { 42 | storage.log = newValue 43 | } 44 | } 45 | 46 | public var willRead: ((GATTReadRequest) -> ATTError?)? { 47 | get { 48 | storage.willRead 49 | } 50 | set { 51 | storage.willRead = newValue 52 | } 53 | } 54 | 55 | public var willWrite: ((GATTWriteRequest) -> ATTError?)? { 56 | get { 57 | storage.willWrite 58 | } 59 | set { 60 | storage.willWrite = newValue 61 | } 62 | } 63 | 64 | public var didWrite: ((GATTWriteConfirmation) -> ())? { 65 | get { 66 | storage.didWrite 67 | } 68 | set { 69 | storage.didWrite = newValue 70 | } 71 | } 72 | 73 | public var connections: Set { 74 | Set(storage.connections.values.lazy.map { $0.central }) 75 | } 76 | 77 | public var isAdvertising: Bool { 78 | storage.isAdvertising 79 | } 80 | 81 | private var _storage = Storage() 82 | 83 | private var storage: Storage { 84 | get { 85 | lock.lock() 86 | defer { lock.unlock() } 87 | return _storage 88 | } 89 | set { 90 | lock.lock() 91 | defer { lock.unlock() } 92 | _storage = newValue 93 | } 94 | } 95 | 96 | private let lock = NSLock() 97 | 98 | // MARK: - Initialization 99 | 100 | public init( 101 | hostController: HostController, 102 | options: Options = Options(), 103 | socket: Socket.Type 104 | ) { 105 | self.hostController = hostController 106 | self.options = options 107 | } 108 | 109 | deinit { 110 | if storage.isAdvertising { 111 | storage.stop() 112 | } 113 | } 114 | 115 | // MARK: - Methods 116 | 117 | public func start() { 118 | guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { 119 | fatalError("Requires Swift concurrency runtime") 120 | } 121 | // ignore errors 122 | Task { 123 | do { try await start(options: .init()) } 124 | catch { 125 | assertionFailure("Unable to start GATT server: \(error)") 126 | } 127 | } 128 | } 129 | 130 | public func start(address: BluetoothAddress, isRandom: Bool) throws(Error) { 131 | // create server socket 132 | let socket: Socket 133 | do { 134 | socket = try Socket.lowEnergyServer( 135 | address: address, 136 | isRandom: isRandom, 137 | backlog: self.options.socketBacklog 138 | ) 139 | } 140 | catch { 141 | throw .connection(.socket(error)) 142 | } 143 | 144 | // start listening for connections 145 | let thread = Thread { [weak self] in 146 | self?.log?("Started GATT Server") 147 | // listen for 148 | while let self = self, self.storage.isAdvertising, let socket = self.storage.socket { 149 | self.accept(socket) 150 | } 151 | } 152 | self.storage.socket = socket 153 | self.storage.thread = thread 154 | thread.start() 155 | } 156 | 157 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 158 | public func start(options: GATTPeripheralAdvertisingOptions) async throws { 159 | assert(isAdvertising == false) 160 | 161 | // use public or random address 162 | let address: BluetoothAddress 163 | if let randomAddress = options.randomAddress { 164 | address = randomAddress 165 | try await hostController.lowEnergySetRandomAddress(randomAddress) 166 | } else { 167 | address = try await hostController.readDeviceAddress() 168 | } 169 | 170 | // set advertising data and scan response 171 | if options.advertisingData != nil || options.scanResponse != nil { 172 | do { try await hostController.enableLowEnergyAdvertising(false) } 173 | catch HCIError.commandDisallowed { /* ignore */ } 174 | } 175 | if let advertisingData = options.advertisingData { 176 | try await hostController.setLowEnergyAdvertisingData(advertisingData) 177 | } 178 | if let scanResponse = options.scanResponse { 179 | try await hostController.setLowEnergyScanResponse(scanResponse) 180 | } 181 | 182 | // enable advertising 183 | do { try await hostController.enableLowEnergyAdvertising() } 184 | catch HCIError.commandDisallowed { /* ignore */ } 185 | 186 | // start listening thread 187 | try start( 188 | address: address, 189 | isRandom: options.randomAddress != nil 190 | ) 191 | } 192 | 193 | public func stop() { 194 | storage.stop() 195 | log?("Stopped GATT Server") 196 | } 197 | 198 | public func add(service: BluetoothGATT.GATTAttribute.Service) -> (UInt16, [UInt16]) { 199 | return storage.add(service: service) 200 | } 201 | 202 | public func remove(service handle: UInt16) { 203 | storage.remove(service: handle) 204 | } 205 | 206 | public func removeAllServices() { 207 | storage.removeAllServices() 208 | } 209 | 210 | /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. 211 | public func write(_ newValue: Data, forCharacteristic handle: UInt16) { 212 | write(newValue, forCharacteristic: handle, ignore: .none) 213 | } 214 | 215 | public func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) throws(Error) { 216 | guard let connection = storage.connections[central] else { 217 | throw .disconnected(central) 218 | } 219 | connection.write(newValue, forCharacteristic: handle) 220 | } 221 | 222 | /// Read the value of the characteristic with specified handle. 223 | public subscript(characteristic handle: UInt16) -> Data { 224 | storage.database[handle: handle].value 225 | } 226 | 227 | public func value(for characteristicHandle: UInt16, central: Central) throws(Error) -> Data { 228 | guard let connection = storage.connections[central] else { 229 | throw .disconnected(central) 230 | } 231 | return connection[characteristicHandle] 232 | } 233 | 234 | /// Return the handles of the characteristics matching the specified UUID. 235 | public func characteristics(for uuid: BluetoothUUID) -> [UInt16] { 236 | return storage.database 237 | .lazy 238 | .filter { $0.uuid == uuid } 239 | .map { $0.handle } 240 | } 241 | } 242 | 243 | internal extension GATTPeripheral { 244 | 245 | func log(_ central: Central, _ message: String) { 246 | log?("[\(central)]: " + message) 247 | } 248 | 249 | /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. 250 | func write(_ newValue: Data, forCharacteristic handle: UInt16, ignore central: Central? = nil) { 251 | // write to master DB 252 | storage.write(newValue, forAttribute: handle) 253 | // propagate changes to active connections 254 | let connections = storage.connections 255 | .values 256 | .lazy 257 | .filter { $0.central != central } 258 | // update the DB of each connection, and send notifications concurrently 259 | for connection in connections { 260 | connection.write(newValue, forCharacteristic: handle) 261 | } 262 | } 263 | 264 | func callback(for central: Central) -> GATTServer.Callback { 265 | var callback = GATTServer.Callback() 266 | callback.willRead = { [weak self] in 267 | self?.willRead(central: central, uuid: $0, handle: $1, value: $2, offset: $3) 268 | } 269 | callback.willWrite = { [weak self] in 270 | self?.willWrite(central: central, uuid: $0, handle: $1, value: $2, newValue: $3) 271 | } 272 | callback.didWrite = { [weak self] (uuid, handle, value) in 273 | self?.didWrite(central: central, uuid: uuid, handle: handle, value: value) 274 | } 275 | return callback 276 | } 277 | 278 | func maximumUpdateValueLength(for central: Central) -> Int { 279 | guard let maximumUpdateValueLength = self.storage.connections[central]?.maximumUpdateValueLength else { 280 | assertionFailure() 281 | return Int(ATTMaximumTransmissionUnit.min.rawValue - 3) 282 | } 283 | return maximumUpdateValueLength 284 | } 285 | 286 | func willRead(central: Central, uuid: BluetoothUUID, handle: UInt16, value: Data, offset: Int) -> ATTError? { 287 | let maximumUpdateValueLength = maximumUpdateValueLength(for: central) 288 | let request = GATTReadRequest( 289 | central: central, 290 | maximumUpdateValueLength: maximumUpdateValueLength, 291 | uuid: uuid, 292 | handle: handle, 293 | value: value, 294 | offset: offset 295 | ) 296 | return willRead?(request) 297 | } 298 | 299 | func willWrite(central: Central, uuid: BluetoothUUID, handle: UInt16, value: Data, newValue: Data) -> ATTError? { 300 | let maximumUpdateValueLength = maximumUpdateValueLength(for: central) 301 | let request = GATTWriteRequest( 302 | central: central, 303 | maximumUpdateValueLength: maximumUpdateValueLength, 304 | uuid: uuid, 305 | handle: handle, 306 | value: value, 307 | newValue: newValue 308 | ) 309 | return willWrite?(request) 310 | } 311 | 312 | func didWrite(central: Central, uuid: BluetoothUUID, handle: UInt16, value: Data) { 313 | let maximumUpdateValueLength = maximumUpdateValueLength(for: central) 314 | let confirmation = GATTWriteConfirmation( 315 | central: central, 316 | maximumUpdateValueLength: maximumUpdateValueLength, 317 | uuid: uuid, 318 | handle: handle, 319 | value: value 320 | ) 321 | // update DB and inform other connections 322 | write(confirmation.value, forCharacteristic: confirmation.handle, ignore: confirmation.central) 323 | // notify delegate 324 | didWrite?(confirmation) 325 | } 326 | 327 | func accept( 328 | _ socket: Socket 329 | ) { 330 | let log = self.log 331 | do { 332 | if Thread.current.isCancelled { 333 | return 334 | } 335 | // wait for pending socket 336 | while socket.status.accept == false, socket.status.error == nil { 337 | Thread.sleep(forTimeInterval: 0.1) 338 | if Thread.current.isCancelled { 339 | return 340 | } 341 | } 342 | let newSocket = try socket.accept() 343 | log?("[\(newSocket.address)]: New connection") 344 | let central = Central(id: socket.address) 345 | let connection = GATTServerConnection( 346 | central: central, 347 | socket: newSocket, 348 | maximumTransmissionUnit: options.maximumTransmissionUnit, 349 | maximumPreparedWrites: options.maximumPreparedWrites, 350 | database: storage.database, 351 | callback: callback(for: central), 352 | log: { 353 | log?("[\(central)]: " + $0) 354 | } 355 | ) 356 | storage.newConnection(connection) 357 | Thread.detachNewThread { [weak connection, weak self] in 358 | do { 359 | while let connection, self != nil { 360 | Thread.sleep(forTimeInterval: 0.01) 361 | // read and write 362 | try connection.run() 363 | } 364 | } 365 | catch { 366 | log?("[\(central)]: " + error.localizedDescription) 367 | } 368 | self?.didDisconnect(central, log: log) 369 | } 370 | } 371 | catch { 372 | log?("Error waiting for new connection: \(error)") 373 | Thread.sleep(forTimeInterval: 1.0) 374 | } 375 | } 376 | 377 | func didDisconnect( 378 | _ central: Central, 379 | log: (@Sendable (String) -> ())? 380 | ) { 381 | // try advertising again 382 | let hostController = self.hostController 383 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 384 | Task { 385 | do { try await hostController.enableLowEnergyAdvertising() } 386 | catch HCIError.commandDisallowed { /* ignore */ } 387 | catch { log?("Could not enable advertising. \(error)") } 388 | } 389 | } 390 | // remove connection cache 391 | storage.removeConnection(central) 392 | // log 393 | log?("[\(central)]: " + "Did disconnect.") 394 | } 395 | } 396 | 397 | // MARK: - Supporting Types 398 | 399 | public struct GATTPeripheralOptions: Equatable, Hashable, Sendable { 400 | 401 | public var maximumTransmissionUnit: ATTMaximumTransmissionUnit 402 | 403 | public var maximumPreparedWrites: Int 404 | 405 | public var socketBacklog: Int 406 | 407 | public init( 408 | maximumTransmissionUnit: ATTMaximumTransmissionUnit = .max, 409 | maximumPreparedWrites: Int = 100, 410 | socketBacklog: Int = 20 411 | ) { 412 | assert(maximumPreparedWrites > 0) 413 | assert(socketBacklog > 0) 414 | self.maximumTransmissionUnit = maximumTransmissionUnit 415 | self.maximumPreparedWrites = maximumPreparedWrites 416 | self.socketBacklog = socketBacklog 417 | } 418 | } 419 | 420 | public struct GATTPeripheralAdvertisingOptions: Equatable, Hashable, Sendable { 421 | 422 | public var advertisingData: LowEnergyAdvertisingData? 423 | 424 | public var scanResponse: LowEnergyAdvertisingData? 425 | 426 | public var randomAddress: BluetoothAddress? 427 | 428 | public init( 429 | advertisingData: LowEnergyAdvertisingData? = nil, 430 | scanResponse: LowEnergyAdvertisingData? = nil, 431 | randomAddress: BluetoothAddress? = nil 432 | ) { 433 | self.advertisingData = advertisingData 434 | self.scanResponse = scanResponse 435 | self.randomAddress = randomAddress 436 | } 437 | } 438 | 439 | public extension GATTPeripheral { 440 | 441 | enum Error: Swift.Error { 442 | 443 | case disconnected(Central) 444 | case connection(ATTConnectionError) 445 | } 446 | } 447 | 448 | internal extension GATTPeripheral { 449 | 450 | struct Storage { 451 | 452 | var database = GATTDatabase() 453 | 454 | var willRead: ((GATTReadRequest) -> ATTError?)? 455 | 456 | var willWrite: ((GATTWriteRequest) -> ATTError?)? 457 | 458 | var didWrite: ((GATTWriteConfirmation) -> ())? 459 | 460 | var log: (@Sendable (String) -> ())? 461 | 462 | var socket: Socket? 463 | 464 | var thread: Thread? 465 | 466 | var connections = [Central: GATTServerConnection](minimumCapacity: 2) 467 | 468 | fileprivate init() { } 469 | 470 | var isAdvertising: Bool { 471 | socket != nil 472 | } 473 | 474 | mutating func stop() { 475 | assert(socket != nil) 476 | socket = nil 477 | thread?.cancel() 478 | thread = nil 479 | } 480 | 481 | mutating func add(service: GATTAttribute.Service) -> (UInt16, [UInt16]) { 482 | var includedServicesHandles = [UInt16]() 483 | var characteristicDeclarationHandles = [UInt16]() 484 | var characteristicValueHandles = [UInt16]() 485 | var descriptorHandles = [[UInt16]]() 486 | let serviceHandle = database.add( 487 | service: service, 488 | includedServicesHandles: &includedServicesHandles, 489 | characteristicDeclarationHandles: &characteristicDeclarationHandles, 490 | characteristicValueHandles: &characteristicValueHandles, 491 | descriptorHandles: &descriptorHandles 492 | ) 493 | return (serviceHandle, characteristicValueHandles) 494 | } 495 | 496 | mutating func remove(service handle: UInt16) { 497 | database.remove(service: handle) 498 | } 499 | 500 | mutating func removeAllServices() { 501 | database.removeAll() 502 | } 503 | 504 | mutating func write(_ value: Data, forAttribute handle: UInt16) { 505 | database.write(value, forAttribute: handle) 506 | } 507 | 508 | mutating func newConnection( 509 | _ connection: GATTServerConnection 510 | ) { 511 | connections[connection.central] = connection 512 | } 513 | 514 | mutating func removeConnection(_ central: Central) { 515 | connections[central] = nil 516 | } 517 | 518 | mutating func maximumUpdateValueLength(for central: Central) -> Int? { 519 | connections[central]?.maximumUpdateValueLength 520 | } 521 | } 522 | } 523 | 524 | #endif 525 | -------------------------------------------------------------------------------- /Tests/GATTTests/GATTTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GATTTests.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 7/12/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if canImport(BluetoothGATT) 10 | import Foundation 11 | import XCTest 12 | import Bluetooth 13 | import BluetoothHCI 14 | @testable import BluetoothGATT 15 | @testable import GATT 16 | 17 | final class GATTTests: XCTestCase { 18 | 19 | typealias TestPeripheral = GATTPeripheral 20 | typealias TestCentral = GATTCentral 21 | 22 | func testScanData() { 23 | 24 | do { 25 | 26 | /** 27 | HCI Event RECV 0x0000 94:E3:6D:62:1E:01 LE Meta Event - LE Advertising Report - 0 - 94:E3:6D:62:1E:01 -65 dBm - Type 2 28 | 29 | Parameter Length: 42 (0x2A) 30 | Num Reports: 0X01 31 | Event Type: Connectable undirected advertising (ADV_IND) 32 | Address Type: Public 33 | Peer Address: 94:E3:6D:62:1E:01 34 | Length Data: 0X1E 35 | Flags: 0X06 36 | Apple Manufacturing Data 37 | Length: 26 (0x1A) 38 | Data: 02 01 06 1A FF 4C 00 02 15 FD A5 06 93 A4 E2 4F B1 AF CF C6 EB 07 64 78 25 27 12 0B 86 BE 39 | RSSI: -65 dBm 40 | */ 41 | 42 | let data = Data([/* 0x3E, 0x2A, 0x02, */ 0x01, 0x00, 0x00, 0x01, 0x1E, 0x62, 0x6D, 0xE3, 0x94, 0x1E, 0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25, 0x27, 0x12, 0x0B, 0x86, 0xBE, 0xBF]) 43 | 44 | guard let advertisingReports = HCILEAdvertisingReport(data: data), 45 | let report = advertisingReports.reports.first 46 | else { XCTFail("Could not parse HCI event"); return } 47 | 48 | XCTAssertEqual(report.event.isConnectable, true) 49 | 50 | let peripheral = Peripheral(id: report.address) 51 | 52 | let scanData = ScanData( 53 | peripheral: peripheral, 54 | date: Date(), 55 | rssi: -65.0, 56 | advertisementData: report.responseData, 57 | isConnectable: report.event.isConnectable 58 | ) 59 | 60 | XCTAssertEqual(scanData.peripheral.id.rawValue, "94:E3:6D:62:1E:01") 61 | } 62 | } 63 | 64 | func testMTUExchange() async throws { 65 | 66 | /** 67 | Exchange MTU Request - MTU:104 68 | Opcode: 0x02 69 | Client Rx MTU: 0x0068 70 | 71 | Exchange MTU Response - MTU:200 72 | Opcode: 0x03 73 | Client Rx MTU: 0x00c8 74 | */ 75 | 76 | let clientMTU = ATTMaximumTransmissionUnit(rawValue: 104)! // 0x0068 77 | let serverMTU = ATTMaximumTransmissionUnit(rawValue: 200)! // 0x00c8 78 | let finalMTU = clientMTU 79 | XCTAssertEqual(ATTMaximumTransmissionUnit(server: clientMTU.rawValue, client: serverMTU.rawValue), finalMTU) 80 | 81 | let testPDUs: [(ATTProtocolDataUnit, [UInt8])] = [ 82 | (ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue), 83 | [0x02, 0x68, 0x00]), 84 | (ATTMaximumTransmissionUnitResponse(serverMTU: serverMTU.rawValue), 85 | [0x03, 0xC8, 0x00]) 86 | ] 87 | 88 | // decode and validate bytes 89 | test(testPDUs) 90 | let mockData = split(pdu: testPDUs.map { $0.1 }) 91 | 92 | try await connect( 93 | serverOptions: GATTPeripheralOptions( 94 | maximumTransmissionUnit: serverMTU, 95 | maximumPreparedWrites: .max 96 | ), 97 | clientOptions: GATTCentralOptions( 98 | maximumTransmissionUnit: clientMTU 99 | ), 100 | client: { (central, peripheral) in 101 | let services = try await central.discoverServices(for: peripheral) 102 | XCTAssertEqual(services.count, 0) 103 | let clientMTU = try await central.maximumTransmissionUnit(for: peripheral) 104 | XCTAssertEqual(clientMTU, finalMTU) 105 | let maximumUpdateValueLength = await central.storage.connections.first?.value.connection.maximumUpdateValueLength 106 | XCTAssertEqual(maximumUpdateValueLength, Int(finalMTU.rawValue) - 3) 107 | let clientCache = await central.storage.connections.values.first?.connection.client.connection.socket.cache 108 | XCTAssertEqual(clientCache?.prefix(1), mockData.client.prefix(1)) // not same because extra service discovery request 109 | } 110 | ) 111 | } 112 | 113 | func testServiceDiscovery() async throws { 114 | 115 | let clientMTU = ATTMaximumTransmissionUnit(rawValue: 104)! // 0x0068 116 | let serverMTU = ATTMaximumTransmissionUnit.default // 23 117 | let finalMTU = serverMTU 118 | XCTAssertEqual(ATTMaximumTransmissionUnit(server: clientMTU.rawValue, client: serverMTU.rawValue), finalMTU) 119 | 120 | let testPDUs: [(ATTProtocolDataUnit, [UInt8])] = [ 121 | /** 122 | Exchange MTU Request - MTU:104 123 | Opcode: 0x02 124 | Client Rx MTU: 0x0068 125 | */ 126 | (ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue), 127 | [0x02, 0x68, 0x00]), 128 | /** 129 | Exchange MTU Response - MTU:23 130 | Opcode: 0x03 131 | Client Rx MTU: 0x0017 132 | */ 133 | (ATTMaximumTransmissionUnitResponse(serverMTU: serverMTU.rawValue), 134 | [0x03, 0x17, 0x00]), 135 | /** 136 | Read By Group Type Request - Start Handle:0x0001 - End Handle:0xffff - UUID:2800 (GATT Primary Service Declaration) 137 | Opcode: 0x10 138 | Starting Handle: 0x0001 139 | Ending Handle: 0xffff 140 | Attribute Group Type: 2800 (GATT Primary Service Declaration) 141 | */ 142 | (ATTReadByGroupTypeRequest(startHandle: 0x0001, endHandle: 0xffff, type: BluetoothUUID.Declaration.primaryService), 143 | [0x10, 0x01, 0x00, 0xFF, 0xFF, 0x00, 0x28]), 144 | /** 145 | Read By Group Type Response 146 | Opcode: 0x11 147 | List Length: 0006 148 | Attribute Handle: 0x0001 End Group Handle: 0x0004 UUID: 180F (Battery Service) 149 | */ 150 | (ATTReadByGroupTypeResponse(attributeData: [ 151 | ATTReadByGroupTypeResponse.AttributeData(attributeHandle: 0x001, 152 | endGroupHandle: 0x0004, 153 | value: Data(BluetoothUUID.Service.battery.littleEndian)) 154 | ])!, 155 | [0x11, 0x06, 0x01, 0x00, 0x04, 0x00, 0x0F, 0x18]), 156 | /** 157 | Read By Group Type Request - Start Handle:0x0005 - End Handle:0xffff - UUID:2800 (GATT Primary Service Declaration) 158 | Opcode: 0x10 159 | Starting Handle: 0x0005 160 | Ending Handle: 0xffff 161 | Attribute Group Type: 2800 (GATT Primary Service Declaration) 162 | */ 163 | (ATTReadByGroupTypeRequest(startHandle: 0x0005, endHandle: 0xffff, type: BluetoothUUID.Declaration.primaryService), 164 | [0x10, 0x05, 0x00, 0xFF, 0xFF, 0x00, 0x28]), 165 | /** 166 | Error Response - Attribute Handle: 0x0005 - Error Code: 0x0A - Attribute Not Found 167 | Opcode: 0x01 168 | Request Opcode In Error: 0x10 (Read By Group Type Request) 169 | Attribute Handle In Error: 0x0005 (5) 170 | Error Code: 0x0a (Attribute Not Found) 171 | */ 172 | (ATTErrorResponse(request: .readByGroupTypeRequest, attributeHandle: 0x0005, error: .attributeNotFound), 173 | [0x01, 0x10, 0x05, 0x00, 0x0A]) 174 | ] 175 | 176 | // decode and validate bytes 177 | test(testPDUs) 178 | let mockData = split(pdu: testPDUs.map { $0.1 }) 179 | 180 | // service 181 | let batteryLevel = GATTBatteryLevel(level: .min) 182 | 183 | let characteristics = [ 184 | GATTAttribute.Characteristic( 185 | uuid: type(of: batteryLevel).uuid, 186 | value: batteryLevel.data, 187 | permissions: [.read], 188 | properties: [.read, .notify], 189 | descriptors: [.init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write])]) 190 | ] 191 | 192 | let service = GATTAttribute.Service( 193 | uuid: BluetoothUUID.Service.battery, 194 | isPrimary: true, 195 | characteristics: characteristics 196 | ) 197 | 198 | try await connect( 199 | serverOptions: GATTPeripheralOptions( 200 | maximumTransmissionUnit: serverMTU, 201 | maximumPreparedWrites: .max 202 | ), 203 | clientOptions: GATTCentralOptions( 204 | maximumTransmissionUnit: clientMTU 205 | ), server: { peripheral in 206 | _ = peripheral.add(service: service) 207 | }, client: { (central, peripheral) in 208 | let services = try await central.discoverServices(for: peripheral) 209 | let clientMTU = try await central.maximumTransmissionUnit(for: peripheral) 210 | XCTAssertEqual(clientMTU, finalMTU) 211 | guard let foundService = services.first, 212 | services.count == 1 213 | else { XCTFail(); return } 214 | XCTAssertEqual(foundService.uuid, BluetoothUUID.Service.battery) 215 | XCTAssertEqual(foundService.isPrimary, true) 216 | let clientCache = await central.storage.connections.values.first?.connection.client.connection.socket.cache 217 | XCTAssertEqual(clientCache, mockData.client) 218 | } 219 | ) 220 | 221 | /* 222 | XCTAssertEqual(peripheral.connections.values.first?.maximumUpdateValueLength, Int(finalMTU.rawValue) - 3) 223 | XCTAssertEqual(central.connections.values.first?.maximumUpdateValueLength, Int(finalMTU.rawValue) - 3) 224 | 225 | // validate GATT PDUs 226 | let mockData = split(pdu: testPDUs.map { $0.1 }) 227 | XCTAssertEqual(serverSocket.cache, mockData.server) 228 | XCTAssertEqual(clientSocket.cache, mockData.client) 229 | */ 230 | } 231 | 232 | func testCharacteristicValue() async throws { 233 | 234 | // service 235 | let batteryLevel = GATTBatteryLevel(level: .max) 236 | 237 | let characteristics = [ 238 | GATTAttribute.Characteristic( 239 | uuid: type(of: batteryLevel).uuid, 240 | value: batteryLevel.data, 241 | permissions: [.read, .write], 242 | properties: [.read, .write], 243 | descriptors: [] 244 | ) 245 | ] 246 | 247 | let service = GATTAttribute.Service( 248 | uuid: BluetoothUUID.Service.battery, 249 | isPrimary: true, 250 | characteristics: characteristics 251 | ) 252 | 253 | let newValue = GATTBatteryLevel(level: .min) 254 | 255 | var serviceAttribute: UInt16! 256 | var characteristicValueHandle: UInt16! 257 | var peripheralDatabaseValue: (() async -> (Data))! 258 | 259 | try await connect( 260 | server: { peripheral in 261 | let (serviceAttributeHandle, characteristicValueHandles) = peripheral.add(service: service) 262 | serviceAttribute = serviceAttributeHandle 263 | XCTAssertEqual(serviceAttribute, 1) 264 | characteristicValueHandle = characteristicValueHandles[0] 265 | peripheralDatabaseValue = { peripheral[characteristic: characteristicValueHandle] } 266 | let currentValue = await peripheralDatabaseValue() 267 | XCTAssertEqual(currentValue, characteristics[0].value) 268 | peripheral.willWrite = { 269 | XCTAssertEqual($0.uuid, BluetoothUUID.Characteristic.batteryLevel) 270 | XCTAssertEqual($0.value, batteryLevel.data) 271 | XCTAssertEqual($0.newValue, newValue.data) 272 | return nil 273 | } 274 | peripheral.didWrite = { 275 | XCTAssertEqual($0.uuid, BluetoothUUID.Characteristic.batteryLevel) 276 | XCTAssertEqual($0.value, newValue.data) 277 | } 278 | }, 279 | client: { (central, peripheral) in 280 | let services = try await central.discoverServices(for: peripheral) 281 | let clientMTU = try await central.maximumTransmissionUnit(for: peripheral) 282 | XCTAssertEqual(clientMTU, .default) 283 | guard let foundService = services.first, 284 | services.count == 1 285 | else { XCTFail(); return } 286 | XCTAssertEqual(foundService.uuid, BluetoothUUID.Service.battery) 287 | XCTAssertEqual(foundService.isPrimary, true) 288 | let foundCharacteristics = try await central.discoverCharacteristics(for: foundService) 289 | guard let foundCharacteristic = foundCharacteristics.first, 290 | foundCharacteristics.count == 1 291 | else { XCTFail(); return } 292 | XCTAssertEqual(foundCharacteristic.uuid, BluetoothUUID.Characteristic.batteryLevel) 293 | XCTAssertEqual(foundCharacteristic.properties, [.read, .write]) 294 | // read value 295 | let characteristicData = try await central.readValue(for: foundCharacteristic) 296 | guard let characteristicValue = GATTBatteryLevel(data: characteristicData) 297 | else { XCTFail(); return } 298 | XCTAssertEqual(characteristicValue, batteryLevel) 299 | // write value 300 | try await central.writeValue(newValue.data, for: foundCharacteristic, withResponse: true) 301 | // validate 302 | let currentValue = await peripheralDatabaseValue() 303 | XCTAssertEqual(currentValue, newValue.data) 304 | XCTAssertNotEqual(currentValue, characteristics[0].value) 305 | XCTAssertNotEqual(currentValue, characteristicValue.data) 306 | } 307 | ) 308 | } 309 | 310 | func testNotification() async throws { 311 | 312 | // service 313 | let batteryLevel = GATTBatteryLevel(level: .max) 314 | 315 | let characteristics = [ 316 | GATTAttribute.Characteristic( 317 | uuid: type(of: batteryLevel).uuid, 318 | value: batteryLevel.data, 319 | permissions: [.read], 320 | properties: [.read, .notify], 321 | descriptors: [.init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write])] 322 | ) 323 | ] 324 | 325 | let service = GATTAttribute.Service( 326 | uuid: BluetoothUUID.Service.battery, 327 | isPrimary: true, 328 | characteristics: characteristics 329 | ) 330 | 331 | let newValue = GATTBatteryLevel(level: .min) 332 | 333 | try await connect( 334 | serverOptions: .init(maximumTransmissionUnit: .default, maximumPreparedWrites: 1000), 335 | clientOptions: .init(maximumTransmissionUnit: .max), 336 | server: { peripheral in 337 | let (serviceAttribute, characteristicValueHandles) = peripheral.add(service: service) 338 | XCTAssertEqual(serviceAttribute, 1) 339 | let characteristicValueHandle = characteristicValueHandles[0] 340 | Task { 341 | try await Task.sleep(nanoseconds: 1_000_000_000) 342 | peripheral.write(newValue.data, forCharacteristic: characteristicValueHandle) 343 | } 344 | }, 345 | client: { (central, peripheral) in 346 | let services = try await central.discoverServices(for: peripheral) 347 | let clientMTU = try await central.maximumTransmissionUnit(for: peripheral) 348 | XCTAssertEqual(clientMTU, .default) 349 | guard let foundService = services.first, 350 | services.count == 1 351 | else { XCTFail(); return } 352 | XCTAssertEqual(foundService.uuid, BluetoothUUID.Service.battery) 353 | XCTAssertEqual(foundService.isPrimary, true) 354 | let foundCharacteristics = try await central.discoverCharacteristics(for: foundService) 355 | guard let foundCharacteristic = foundCharacteristics.first, 356 | foundCharacteristics.count == 1 357 | else { XCTFail(); return } 358 | XCTAssertEqual(foundCharacteristic.uuid, BluetoothUUID.Characteristic.batteryLevel) 359 | XCTAssertEqual(foundCharacteristic.properties, [.read, .notify]) 360 | // wait for notifications 361 | let stream = central.notify(for: foundCharacteristic) 362 | for try await notification in stream { 363 | guard let notificationValue = GATTBatteryLevel(data: notification) else { 364 | XCTFail(); 365 | return 366 | } 367 | XCTAssertEqual(notificationValue, newValue) 368 | stream.stop() 369 | break 370 | } 371 | } 372 | ) 373 | } 374 | 375 | func testIndication() async throws { 376 | 377 | // service 378 | let batteryLevel = GATTBatteryLevel(level: .max) 379 | 380 | let characteristics = [ 381 | GATTAttribute.Characteristic( 382 | uuid: type(of: batteryLevel).uuid, 383 | value: batteryLevel.data, 384 | permissions: [.read], 385 | properties: [.read, .indicate], 386 | descriptors: [.init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write])] 387 | ) 388 | ] 389 | 390 | let service = GATTAttribute.Service( 391 | uuid: BluetoothUUID.Service.battery, 392 | isPrimary: true, 393 | characteristics: characteristics 394 | ) 395 | 396 | let newValue = GATTBatteryLevel(level: .min) 397 | 398 | try await connect( 399 | serverOptions: .init(maximumTransmissionUnit: .default, maximumPreparedWrites: 1000), 400 | clientOptions: .init(maximumTransmissionUnit: .max), 401 | server: { peripheral in 402 | let (serviceAttribute, characteristicValueHandles) = peripheral.add(service: service) 403 | XCTAssertEqual(serviceAttribute, 1) 404 | let characteristicValueHandle = characteristicValueHandles[0] 405 | Task { 406 | try await Task.sleep(nanoseconds: 1_000_000_000) 407 | peripheral.write(newValue.data, forCharacteristic: characteristicValueHandle) 408 | } 409 | }, 410 | client: { (central, peripheral) in 411 | let services = try await central.discoverServices(for: peripheral) 412 | let clientMTU = try await central.maximumTransmissionUnit(for: peripheral) 413 | XCTAssertEqual(clientMTU, .default) 414 | guard let foundService = services.first, 415 | services.count == 1 416 | else { XCTFail(); return } 417 | XCTAssertEqual(foundService.uuid, BluetoothUUID.Service.battery) 418 | XCTAssertEqual(foundService.isPrimary, true) 419 | let foundCharacteristics = try await central.discoverCharacteristics(for: foundService) 420 | guard let foundCharacteristic = foundCharacteristics.first, 421 | foundCharacteristics.count == 1 422 | else { XCTFail(); return } 423 | XCTAssertEqual(foundCharacteristic.uuid, BluetoothUUID.Characteristic.batteryLevel) 424 | XCTAssertEqual(foundCharacteristic.properties, [.read, .indicate]) 425 | // wait for notifications 426 | let stream = central.notify(for: foundCharacteristic) 427 | for try await notification in stream { 428 | guard let notificationValue = GATTBatteryLevel(data: notification) else { 429 | XCTFail(); 430 | return 431 | } 432 | XCTAssertEqual(notificationValue, newValue) 433 | stream.stop() 434 | break 435 | } 436 | } 437 | ) 438 | } 439 | 440 | func testDescriptors() async throws { 441 | 442 | let descriptors = [ 443 | .init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write]), 444 | //GATTUserDescription(userDescription: "Characteristic").descriptor, 445 | GATTAttribute.Descriptor(uuid: BluetoothUUID(), 446 | value: Data("UInt128 Descriptor".utf8), 447 | permissions: [.read, .write]), 448 | GATTAttribute.Descriptor(uuid: BluetoothUUID.Member.savantSystems, 449 | value: Data("Savant".utf8), 450 | permissions: [.read]), 451 | GATTAttribute.Descriptor(uuid: BluetoothUUID.Member.savantSystems2, 452 | value: Data("Savant2".utf8), 453 | permissions: [.write]) 454 | ] 455 | 456 | let characteristic = GATTAttribute.Characteristic(uuid: BluetoothUUID(), 457 | value: Data(), 458 | permissions: [.read], 459 | properties: [.read], 460 | descriptors: descriptors) 461 | 462 | let service = GATTAttribute.Service( 463 | uuid: BluetoothUUID(), 464 | isPrimary: true, 465 | characteristics: [characteristic] 466 | ) 467 | 468 | try await connect( 469 | serverOptions: .init(maximumTransmissionUnit: .default, maximumPreparedWrites: 1000), 470 | clientOptions: .init(maximumTransmissionUnit: .default), 471 | server: { peripheral in 472 | let (serviceAttribute, _) = peripheral.add(service: service) 473 | XCTAssertEqual(serviceAttribute, 1) 474 | }, 475 | client: { (central, peripheral) in 476 | let services = try await central.discoverServices(for: peripheral) 477 | let clientMTU = try await central.maximumTransmissionUnit(for: peripheral) 478 | XCTAssertEqual(clientMTU, .default) 479 | guard let foundService = services.first, 480 | services.count == 1 481 | else { XCTFail(); return } 482 | XCTAssertEqual(foundService.uuid, service.uuid) 483 | XCTAssertEqual(foundService.isPrimary, true) 484 | 485 | let foundCharacteristics = try await central.discoverCharacteristics(for: foundService) 486 | XCTAssertEqual(foundCharacteristics.count, 1) 487 | guard let foundCharacteristic = foundCharacteristics.first else { XCTFail(); return } 488 | XCTAssertEqual(foundCharacteristic.uuid, characteristic.uuid) 489 | let foundDescriptors = try await central.discoverDescriptors(for: foundCharacteristic) 490 | XCTAssertEqual(foundDescriptors.count, descriptors.count) 491 | XCTAssertEqual(foundDescriptors.map { $0.uuid }, descriptors.map { $0.uuid }) 492 | 493 | for (index, descriptor) in foundDescriptors.enumerated() { 494 | let expectedValue = descriptors[index].value 495 | let descriptorPermissions = descriptors[index].permissions 496 | if descriptorPermissions.contains(.read) { 497 | let readValue = try await central.readValue(for: descriptor) 498 | XCTAssertEqual(readValue, expectedValue) 499 | } 500 | if descriptorPermissions.contains(.write) { 501 | let newValue = Data("new value".utf8) 502 | try await central.writeValue(newValue, for: descriptor) 503 | if descriptorPermissions.contains(.read) { 504 | let newServerValue = try await central.readValue(for: descriptor) 505 | XCTAssertEqual(newValue, newServerValue) 506 | } 507 | } 508 | } 509 | } 510 | ) 511 | } 512 | 513 | } 514 | 515 | extension GATTTests { 516 | 517 | func connect( 518 | serverOptions: GATTPeripheralOptions = .init(), 519 | clientOptions: GATTCentralOptions = .init(), 520 | advertisingReports: [Data] = [ 521 | Data([0x3E, 0x2A, 0x02, 0x01, 0x00, 0x00, 0x01, 0x1E, 0x62, 0x6D, 0xE3, 0x94, 0x1E, 0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25, 0x27, 0x12, 0x0B, 0x86, 0xBE, 0xBF]) 522 | ], 523 | server: (TestPeripheral) async throws -> () = { _ in }, 524 | client: (TestCentral, Peripheral) async throws -> (), 525 | file: StaticString = #file, 526 | line: UInt = #line 527 | ) async throws { 528 | 529 | guard let reportData = advertisingReports.first?.suffix(from: 3), 530 | let report = HCILEAdvertisingReport(data: Data(reportData)) else { 531 | XCTFail("No scanned devices", file: file, line: line) 532 | return 533 | } 534 | 535 | // host controller 536 | let serverHostController = TestHostController(address: report.reports.first!.address) 537 | let clientHostController = TestHostController(address: .min) 538 | 539 | // peripheral 540 | let peripheral = TestPeripheral( 541 | hostController: serverHostController, 542 | options: serverOptions, 543 | socket: TestL2CAPServer.self 544 | ) 545 | peripheral.log = { print("Peripheral:", $0) } 546 | try await server(peripheral) 547 | 548 | peripheral.start() 549 | defer { peripheral.stop() } 550 | 551 | // central 552 | let central = TestCentral( 553 | hostController: clientHostController, 554 | options: clientOptions, 555 | socket: TestL2CAPSocket.self 556 | ) 557 | central.log = { print("Central:", $0) } 558 | central.hostController.advertisingReports = advertisingReports 559 | 560 | let scan = try await central.scan(filterDuplicates: true) 561 | guard let device = try await scan.first() 562 | else { XCTFail("No devices scanned"); return } 563 | 564 | // connect and execute 565 | try await central.connect(to: device.peripheral) 566 | try await client(central, device.peripheral) 567 | // cleanup 568 | await central.disconnectAll() 569 | peripheral.removeAllServices() 570 | } 571 | 572 | func test( 573 | _ testPDUs: [(ATTProtocolDataUnit, [UInt8])], 574 | file: StaticString = #filePath, 575 | line: UInt = #line 576 | ) { 577 | 578 | // decode and compare 579 | for (testPDU, testData) in testPDUs { 580 | 581 | guard let decodedPDU = type(of: testPDU).init(data: testData) 582 | else { XCTFail("Could not decode \(type(of: testPDU))"); return } 583 | 584 | XCTAssertEqual(Data(decodedPDU), Data(testData), file: file, line: line) 585 | } 586 | } 587 | 588 | func split(pdu data: [[UInt8]]) -> (server: [Data], client: [Data]) { 589 | 590 | var serverSocketData = [Data]() 591 | var clientSocketData = [Data]() 592 | 593 | for pduData in data { 594 | 595 | guard let opcodeByte = pduData.first 596 | else { fatalError("Empty data \(pduData)") } 597 | 598 | guard let opcode = ATTOpcode(rawValue: opcodeByte) 599 | else { fatalError("Invalid opcode \(opcodeByte)") } 600 | 601 | switch opcode.type.destination { 602 | case .client: 603 | clientSocketData.append(Data(pduData)) 604 | case .server: 605 | serverSocketData.append(Data(pduData)) 606 | } 607 | } 608 | 609 | return (serverSocketData, clientSocketData) 610 | } 611 | } 612 | 613 | fileprivate extension ATTOpcodeType { 614 | 615 | enum Destination { 616 | 617 | case client 618 | case server 619 | } 620 | 621 | var destination: Destination { 622 | 623 | switch self { 624 | case .command, 625 | .request: 626 | return .server 627 | case .response, 628 | .confirmation, 629 | .indication, 630 | .notification: 631 | return .client 632 | } 633 | } 634 | } 635 | 636 | #endif 637 | -------------------------------------------------------------------------------- /Sources/DarwinGATT/DarwinPeripheral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarwinPeripheral.swift 3 | // GATT 4 | // 5 | // Created by Alsey Coleman Miller on 4/1/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #if (os(macOS) || os(iOS)) && canImport(BluetoothGATT) 10 | import Foundation 11 | import Dispatch 12 | import Bluetooth 13 | import BluetoothGATT 14 | import GATT 15 | @preconcurrency import CoreBluetooth 16 | import CoreLocation 17 | 18 | public final class DarwinPeripheral: PeripheralManager, @unchecked Sendable { 19 | 20 | public typealias Error = Swift.Error 21 | 22 | // MARK: - Properties 23 | 24 | /// Logging 25 | public var log: (@Sendable (String) -> ())? 26 | 27 | public let options: Options 28 | 29 | public var state: DarwinBluetoothState { 30 | unsafeBitCast(self.peripheralManager.state, to: DarwinBluetoothState.self) 31 | } 32 | 33 | public var isAdvertising: Bool { 34 | self.peripheralManager.isAdvertising 35 | } 36 | 37 | public var willRead: ((GATTReadRequest) -> ATTError?)? 38 | 39 | public var willWrite: ((GATTWriteRequest) -> ATTError?)? 40 | 41 | public var didWrite: ((GATTWriteConfirmation) -> ())? 42 | 43 | public var stateChanged: ((DarwinBluetoothState) -> ())? 44 | 45 | private var database = Database() 46 | 47 | private var peripheralManager: CBPeripheralManager! 48 | 49 | private var delegate: Delegate! 50 | 51 | private let queue: DispatchQueue = .main 52 | 53 | private var _continuation: Any? 54 | 55 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 56 | private var continuation: Continuation { 57 | get { 58 | _continuation as! Continuation 59 | } 60 | set { 61 | _continuation = newValue 62 | } 63 | } 64 | 65 | // MARK: - Initialization 66 | 67 | public init( 68 | options: Options = Options(showPowerAlert: false) 69 | ) { 70 | self.options = options 71 | let delegate = Delegate(self) 72 | let peripheralManager = CBPeripheralManager( 73 | delegate: delegate, 74 | queue: queue, 75 | options: options.options 76 | ) 77 | self.delegate = delegate 78 | self.peripheralManager = peripheralManager 79 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 80 | self.continuation = Continuation() 81 | } 82 | } 83 | 84 | // MARK: - Methods 85 | 86 | public func start() { 87 | let options = AdvertisingOptions() 88 | self.peripheralManager.startAdvertising(options.options) 89 | } 90 | 91 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 92 | public func start(options: AdvertisingOptions) async throws { 93 | return try await withCheckedThrowingContinuation { [unowned self] continuation in 94 | self.queue.async { [unowned self] in 95 | self.continuation.startAdvertising = continuation 96 | self.peripheralManager.startAdvertising(options.options) 97 | } 98 | } 99 | } 100 | 101 | public func stop() { 102 | peripheralManager.stopAdvertising() 103 | } 104 | 105 | public func add(service: GATTAttribute.Service) -> (UInt16, [UInt16]) { 106 | // add service 107 | let serviceObject = service.toCoreBluetooth() 108 | peripheralManager.add(serviceObject) 109 | let handle = database.add(service: service, serviceObject) 110 | return handle 111 | } 112 | 113 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 114 | public func add(service: GATTAttribute.Service) async throws -> (UInt16, [UInt16]) { 115 | let serviceObject = service.toCoreBluetooth() 116 | // add service 117 | try await withCheckedThrowingContinuation { [unowned self] (continuation: CheckedContinuation<(), Error>) in 118 | self.queue.async { [unowned self] in 119 | self.continuation.addService = continuation 120 | peripheralManager.add(serviceObject) 121 | } 122 | } 123 | // update DB 124 | return await withUnsafeContinuation { [unowned self] continuation in 125 | self.queue.async { [unowned self] in 126 | let handle = database.add(service: service, serviceObject) 127 | continuation.resume(returning: handle) 128 | } 129 | } 130 | } 131 | 132 | public func remove(service handle: UInt16) { 133 | let serviceObject = database.service(for: handle) 134 | peripheralManager.remove(serviceObject) 135 | // remove from cache 136 | database.remove(service: handle) 137 | } 138 | 139 | public func removeAllServices() { 140 | // remove from daemon 141 | peripheralManager.removeAllServices() 142 | // clear cache 143 | database.removeAll() 144 | } 145 | 146 | /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. 147 | public func write(_ newValue: Data, forCharacteristic handle: UInt16) { 148 | // update GATT DB 149 | database[characteristic: handle] = newValue 150 | // send notifications 151 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 152 | Task { 153 | // attempt to write notifications 154 | var didNotify = updateValue(newValue, forCharacteristic: handle) 155 | while didNotify == false { 156 | await waitPeripheralReadyUpdateSubcribers() 157 | didNotify = updateValue(newValue, forCharacteristic: handle) 158 | } 159 | } 160 | } else { 161 | updateValue(newValue, forCharacteristic: handle) 162 | } 163 | } 164 | 165 | public func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) { 166 | write(newValue, forCharacteristic: handle) // per-connection database not supported on Darwin 167 | } 168 | 169 | public func value(for characteristicHandle: UInt16, central: Central) -> Data { 170 | self[characteristic: characteristicHandle] 171 | } 172 | 173 | /// Read the value of the characteristic with specified handle. 174 | public subscript(characteristic handle: UInt16) -> Data { 175 | self.database[characteristic: handle] 176 | } 177 | 178 | public subscript(characteristic handle: UInt16, central: Central) -> Data { 179 | self[characteristic: handle] // per-connection database not supported on Darwin 180 | } 181 | 182 | /// Return the handles of the characteristics matching the specified UUID. 183 | public func characteristics(for uuid: BluetoothUUID) -> [UInt16] { 184 | database.characteristics(for: uuid) 185 | } 186 | 187 | public func setDesiredConnectionLatency(_ latency: CBPeripheralManagerConnectionLatency, for central: Central) { 188 | guard let central = central.central else { 189 | assertionFailure() 190 | return 191 | } 192 | self.peripheralManager.setDesiredConnectionLatency(latency, for: central) 193 | } 194 | 195 | // MARK: - Private Methods 196 | 197 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 198 | private func waitPeripheralReadyUpdateSubcribers() async { 199 | await withCheckedContinuation { [unowned self] continuation in 200 | self.queue.async { [unowned self] in 201 | self.continuation.canNotify = continuation 202 | } 203 | } 204 | } 205 | 206 | @discardableResult 207 | private func updateValue(_ value: Data, forCharacteristic handle: UInt16, centrals: [Central] = []) -> Bool { 208 | let characteristicObject = database.characteristic(for: handle) 209 | // sends an updated characteristic value to one or more subscribed centrals, via a notification or indication. 210 | let didNotify = peripheralManager.updateValue( 211 | value, 212 | for: characteristicObject, 213 | onSubscribedCentrals: centrals.isEmpty ? nil : centrals.compactMap { $0.central } 214 | ) 215 | 216 | // The underlying transmit queue is full 217 | if didNotify == false { 218 | // send later in `peripheralManagerIsReady(toUpdateSubscribers:)` method is invoked 219 | // when more space in the transmit queue becomes available. 220 | //log("Did queue notification for \((characteristic as CBCharacteristic).uuid)") 221 | } else { 222 | //log("Did send notification for \((characteristic as CBCharacteristic).uuid)") 223 | } 224 | return didNotify 225 | } 226 | } 227 | 228 | // MARK: - Supporting Types 229 | 230 | public extension DarwinPeripheral { 231 | 232 | /// Peripheral Peer 233 | /// 234 | /// Represents a remote peripheral device that has been discovered. 235 | struct Central: Peer, Identifiable, Equatable, Hashable, CustomStringConvertible { 236 | 237 | public let id: UUID 238 | 239 | internal weak var central: CBCentral? 240 | 241 | init(_ central: CBCentral) { 242 | self.id = central.id 243 | self.central = central 244 | } 245 | } 246 | } 247 | 248 | public extension DarwinPeripheral { 249 | 250 | struct Options: Equatable, Hashable { 251 | 252 | internal let options: [String: NSObject] 253 | 254 | public init() { 255 | self.options = [:] 256 | } 257 | } 258 | } 259 | 260 | public extension DarwinPeripheral.Options { 261 | 262 | var showPowerAlert: Bool { 263 | (options[CBPeripheralManagerOptionShowPowerAlertKey] as? Bool) ?? false 264 | } 265 | 266 | var restoreIdentifier: String? { 267 | options[CBPeripheralManagerOptionRestoreIdentifierKey] as? String 268 | } 269 | 270 | init( 271 | showPowerAlert: Bool, 272 | restoreIdentifier: String? = Bundle.main.bundleIdentifier ?? "org.pureswift.GATT.DarwinPeripheral" 273 | ) { 274 | var options = [String: NSObject](minimumCapacity: 2) 275 | if showPowerAlert { 276 | options[CBPeripheralManagerOptionShowPowerAlertKey] = showPowerAlert as NSNumber 277 | } 278 | options[CBPeripheralManagerOptionRestoreIdentifierKey] = restoreIdentifier as NSString? 279 | self.options = options 280 | } 281 | } 282 | 283 | extension DarwinPeripheral.Options: ExpressibleByDictionaryLiteral { 284 | 285 | public init(dictionaryLiteral elements: (String, NSObject)...) { 286 | self.options = .init(uniqueKeysWithValues: elements) 287 | } 288 | } 289 | 290 | extension DarwinPeripheral.Options: CustomStringConvertible { 291 | 292 | public var description: String { 293 | return (options as NSDictionary).description 294 | } 295 | } 296 | 297 | public extension DarwinPeripheral { 298 | 299 | struct AdvertisingOptions: Equatable, Hashable, @unchecked Sendable { 300 | 301 | internal let options: [String: NSObject] 302 | 303 | public init() { 304 | self.options = [:] 305 | } 306 | } 307 | } 308 | 309 | extension DarwinPeripheral.AdvertisingOptions: ExpressibleByDictionaryLiteral { 310 | 311 | public init(dictionaryLiteral elements: (String, NSObject)...) { 312 | self.options = .init(uniqueKeysWithValues: elements) 313 | } 314 | } 315 | 316 | extension DarwinPeripheral.AdvertisingOptions: CustomStringConvertible { 317 | 318 | public var description: String { 319 | return (options as NSDictionary).description 320 | } 321 | } 322 | 323 | public extension DarwinPeripheral.AdvertisingOptions { 324 | 325 | /// The local name of the peripheral. 326 | var localName: String? { 327 | options[CBAdvertisementDataLocalNameKey] as? String 328 | } 329 | 330 | /// An array of service UUIDs. 331 | var serviceUUIDs: [BluetoothUUID] { 332 | (options[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID])?.map { BluetoothUUID($0) } ?? [] 333 | } 334 | 335 | init( 336 | localName: String? = nil, 337 | serviceUUIDs: [BluetoothUUID] = [], 338 | beacon: AppleBeacon? = nil 339 | ) { 340 | var options = [String: NSObject](minimumCapacity: 5) 341 | if let localName = localName { 342 | options[CBAdvertisementDataLocalNameKey] = localName as NSString 343 | } 344 | if serviceUUIDs.isEmpty == false { 345 | options[CBAdvertisementDataServiceUUIDsKey] = serviceUUIDs.map { CBUUID($0) } as NSArray 346 | } 347 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), let beacon = beacon { 348 | let beaconRegion = CLBeaconRegion( 349 | uuid: beacon.uuid, 350 | major: beacon.major, 351 | minor: beacon.minor, 352 | identifier: beacon.uuid.uuidString 353 | ) 354 | let peripheralData = beaconRegion.peripheralData(withMeasuredPower: NSNumber(value: beacon.rssi)) as! [String: NSObject] 355 | peripheralData.forEach { (key, value) in 356 | options[key] = value 357 | } 358 | } 359 | self.options = options 360 | assert(localName == self.localName) 361 | assert(serviceUUIDs == self.serviceUUIDs) 362 | } 363 | } 364 | 365 | internal extension DarwinPeripheral { 366 | 367 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 368 | struct Continuation { 369 | 370 | var startAdvertising: CheckedContinuation<(), Error>? 371 | 372 | var addService: CheckedContinuation<(), Error>? 373 | 374 | var canNotify: CheckedContinuation<(), Never>? 375 | 376 | fileprivate init() { } 377 | } 378 | } 379 | 380 | internal extension DarwinPeripheral { 381 | 382 | @preconcurrency 383 | @objc(DarwinPeripheralDelegate) 384 | final class Delegate: NSObject, CBPeripheralManagerDelegate, @unchecked Sendable { 385 | 386 | unowned let peripheral: DarwinPeripheral 387 | 388 | init(_ peripheral: DarwinPeripheral) { 389 | self.peripheral = peripheral 390 | } 391 | 392 | private func log(_ message: String) { 393 | peripheral.log?(message) 394 | } 395 | 396 | // MARK: - CBPeripheralManagerDelegate 397 | 398 | @objc(peripheralManagerDidUpdateState:) 399 | public func peripheralManagerDidUpdateState(_ peripheralManager: CBPeripheralManager) { 400 | let state = unsafeBitCast(peripheralManager.state, to: DarwinBluetoothState.self) 401 | log("Did update state \(state)") 402 | self.peripheral.stateChanged?(state) 403 | } 404 | 405 | @objc(peripheralManager:willRestoreState:) 406 | public func peripheralManager(_ peripheralManager: CBPeripheralManager, willRestoreState state: [String : Any]) { 407 | log("Will restore state \(state)") 408 | } 409 | 410 | @objc(peripheralManagerDidStartAdvertising:error:) 411 | public func peripheralManagerDidStartAdvertising(_ peripheralManager: CBPeripheralManager, error: Error?) { 412 | if let error = error { 413 | log("Could not advertise (\(error))") 414 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 415 | self.peripheral.continuation.startAdvertising?.resume(throwing: error) 416 | } 417 | 418 | } else { 419 | log("Did start advertising") 420 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 421 | self.peripheral.continuation.startAdvertising?.resume() 422 | } 423 | } 424 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 425 | self.peripheral.continuation.startAdvertising = nil 426 | } 427 | } 428 | 429 | @objc(peripheralManager:didAddService:error:) 430 | public func peripheralManager(_ peripheralManager: CBPeripheralManager, didAdd service: CBService, error: Error?) { 431 | if let error = error { 432 | log("Could not add service \(service.uuid) (\(error))") 433 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 434 | self.peripheral.continuation.addService?.resume(throwing: error) 435 | } 436 | } else { 437 | log("Added service \(service.uuid)") 438 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 439 | self.peripheral.continuation.addService?.resume() 440 | } 441 | } 442 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 443 | self.peripheral.continuation.addService = nil 444 | } 445 | } 446 | 447 | @objc(peripheralManager:didReceiveReadRequest:) 448 | public func peripheralManager(_ peripheralManager: CBPeripheralManager, didReceiveRead request: CBATTRequest) { 449 | 450 | log("Did receive read request for \(request.characteristic.uuid)") 451 | 452 | let peer = Central(request.central) 453 | let characteristic = self.peripheral.database[characteristic: request.characteristic] 454 | let uuid = BluetoothUUID(request.characteristic.uuid) 455 | let value = characteristic.value 456 | let readRequest = GATTReadRequest( 457 | central: peer, 458 | maximumUpdateValueLength: request.central.maximumUpdateValueLength, 459 | uuid: uuid, 460 | handle: characteristic.handle, 461 | value: value, 462 | offset: request.offset 463 | ) 464 | 465 | guard request.offset <= value.count else { 466 | peripheralManager.respond(to: request, withResult: .invalidOffset) 467 | return 468 | } 469 | if let error = self.peripheral.willRead?(readRequest) { 470 | peripheralManager.respond(to: request, withResult: CBATTError.Code(rawValue: Int(error.rawValue))!) 471 | return 472 | } 473 | 474 | let requestedValue = request.offset == 0 ? value : Data(value.suffix(request.offset)) 475 | request.value = requestedValue 476 | peripheralManager.respond(to: request, withResult: .success) 477 | } 478 | 479 | @objc(peripheralManager:didReceiveWriteRequests:) 480 | public func peripheralManager(_ peripheralManager: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { 481 | 482 | log("Did receive write requests for \(requests.map { $0.characteristic.uuid })") 483 | assert(requests.isEmpty == false) 484 | 485 | guard let firstRequest = requests.first else { 486 | assertionFailure() 487 | return 488 | } 489 | 490 | let writeRequests: [GATTWriteRequest] = requests.map { request in 491 | let peer = Central(request.central) 492 | let characteristic = self.peripheral.database[characteristic: request.characteristic] 493 | let value = characteristic.value 494 | let uuid = BluetoothUUID(request.characteristic.uuid) 495 | let newValue = request.value ?? Data() 496 | return GATTWriteRequest( 497 | central: peer, 498 | maximumUpdateValueLength: request.central.maximumUpdateValueLength, 499 | uuid: uuid, 500 | handle: characteristic.handle, 501 | value: value, 502 | newValue: newValue 503 | ) 504 | } 505 | 506 | let process: () -> (CBATTError.Code) = { [unowned self] in 507 | 508 | // validate write requests 509 | for writeRequest in writeRequests { 510 | 511 | // check if write is possible 512 | if let error = self.peripheral.willWrite?(writeRequest) { 513 | guard let code = CBATTError.Code(rawValue: Int(error.rawValue)) else { 514 | assertionFailure("Invalid CBATTError: \(error.rawValue)") 515 | return CBATTError.Code.unlikelyError 516 | } 517 | return code 518 | } 519 | } 520 | 521 | // write new values 522 | for request in writeRequests { 523 | // update GATT DB 524 | self.peripheral.database[characteristic: request.handle] = request.newValue 525 | let confirmation = GATTWriteConfirmation( 526 | central: request.central, 527 | maximumUpdateValueLength: request.maximumUpdateValueLength, 528 | uuid: request.uuid, 529 | handle: request.handle, 530 | value: request.newValue 531 | ) 532 | // did write callback 533 | self.peripheral.didWrite?(confirmation) 534 | } 535 | 536 | return CBATTError.Code.success 537 | } 538 | 539 | let result = process() 540 | peripheralManager.respond(to: firstRequest, withResult: result) 541 | } 542 | 543 | @objc(peripheralManager:central:didSubscribeToCharacteristic:) 544 | public func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { 545 | log("Central \(central.id) did subscribe to \(characteristic.uuid)") 546 | } 547 | 548 | @objc 549 | public func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) { 550 | log("Central \(central.id) did unsubscribe from \(characteristic.uuid)") 551 | } 552 | 553 | @objc 554 | public func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { 555 | log("Ready to send notifications") 556 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 557 | self.peripheral.continuation.canNotify?.resume() 558 | self.peripheral.continuation.canNotify = nil 559 | } 560 | } 561 | } 562 | } 563 | 564 | private extension DarwinPeripheral { 565 | 566 | struct Database: Sendable { 567 | 568 | struct Service: Sendable { 569 | 570 | let handle: UInt16 571 | } 572 | 573 | struct Characteristic: Sendable { 574 | 575 | let handle: UInt16 576 | 577 | let serviceHandle: UInt16 578 | 579 | var value: Data 580 | } 581 | 582 | private var services = [CBMutableService: Service]() 583 | 584 | private var characteristics = [CBMutableCharacteristic: Characteristic]() 585 | 586 | /// Do not access directly, use `newHandle()` 587 | private var lastHandle: UInt16 = 0x0000 588 | 589 | /// Simulate a GATT database. 590 | private mutating func newHandle() -> UInt16 { 591 | 592 | assert(lastHandle != .max) 593 | 594 | // starts at 0x0001 595 | lastHandle += 1 596 | 597 | return lastHandle 598 | } 599 | 600 | mutating func add(service: GATTAttribute.Service, _ coreService: CBMutableService) -> (UInt16, [UInt16]) { 601 | 602 | let serviceHandle = newHandle() 603 | var characteristicHandles = [UInt16]() 604 | characteristicHandles.reserveCapacity((coreService.characteristics ?? []).count) 605 | services[coreService] = Service(handle: serviceHandle) 606 | for (index, characteristic) in ((coreService.characteristics ?? []) as! [CBMutableCharacteristic]).enumerated() { 607 | let data = service.characteristics[index].value 608 | let characteristicHandle = newHandle() 609 | characteristics[characteristic] = Characteristic( 610 | handle: characteristicHandle, 611 | serviceHandle: serviceHandle, 612 | value: data 613 | ) 614 | characteristicHandles.append(characteristicHandle) 615 | } 616 | return (serviceHandle, characteristicHandles) 617 | } 618 | 619 | mutating func remove(service handle: UInt16) { 620 | 621 | let coreService = service(for: handle) 622 | 623 | // remove service 624 | services[coreService] = nil 625 | (coreService.characteristics as? [CBMutableCharacteristic])?.forEach { characteristics[$0] = nil } 626 | 627 | // remove characteristics 628 | while let index = characteristics.firstIndex(where: { $0.value.serviceHandle == handle }) { 629 | characteristics.remove(at: index) 630 | } 631 | } 632 | 633 | mutating func removeAll() { 634 | 635 | services.removeAll() 636 | characteristics.removeAll() 637 | } 638 | 639 | /// Find the service with the specified handle 640 | func service(for handle: UInt16) -> CBMutableService { 641 | 642 | guard let coreService = services.first(where: { $0.value.handle == handle })?.key 643 | else { fatalError("Invalid handle \(handle)") } 644 | 645 | return coreService 646 | } 647 | 648 | /// Return the handles of the characteristics matching the specified UUID. 649 | func characteristics(for uuid: BluetoothUUID) -> [UInt16] { 650 | 651 | let characteristicUUID = CBUUID(uuid) 652 | 653 | return characteristics 654 | .filter { $0.key.uuid == characteristicUUID } 655 | .map { $0.value.handle } 656 | } 657 | 658 | func characteristic(for handle: UInt16) -> CBMutableCharacteristic { 659 | 660 | guard let characteristic = characteristics.first(where: { $0.value.handle == handle })?.key 661 | else { fatalError("Invalid handle \(handle)") } 662 | 663 | return characteristic 664 | } 665 | 666 | subscript(characteristic handle: UInt16) -> Data { 667 | 668 | get { 669 | 670 | guard let value = characteristics.values.first(where: { $0.handle == handle })?.value 671 | else { fatalError("Invalid handle \(handle)") } 672 | 673 | return value 674 | } 675 | 676 | set { 677 | 678 | guard let key = characteristics.first(where: { $0.value.handle == handle })?.key 679 | else { fatalError("Invalid handle \(handle)") } 680 | 681 | characteristics[key]?.value = newValue 682 | } 683 | } 684 | 685 | subscript(characteristic uuid: BluetoothUUID) -> Data { 686 | 687 | get { 688 | 689 | let characteristicUUID = CBUUID(uuid) 690 | 691 | guard let characteristic = characteristics.first(where: { $0.key.uuid == characteristicUUID })?.value 692 | else { fatalError("Invalid UUID \(uuid)") } 693 | 694 | return characteristic.value 695 | } 696 | 697 | set { 698 | 699 | let characteristicUUID = CBUUID(uuid) 700 | 701 | guard let key = characteristics.keys.first(where: { $0.uuid == characteristicUUID }) 702 | else { fatalError("Invalid UUID \(uuid)") } 703 | 704 | characteristics[key]?.value = newValue 705 | } 706 | } 707 | 708 | private(set) subscript(characteristic characteristic: CBCharacteristic) -> Characteristic { 709 | 710 | get { 711 | 712 | guard let key = characteristic as? CBMutableCharacteristic 713 | else { fatalError("Invalid key") } 714 | 715 | guard let value = characteristics[key] 716 | else { fatalError("No stored characteristic matches \(characteristic)") } 717 | 718 | return value 719 | } 720 | 721 | set { 722 | 723 | guard let key = characteristic as? CBMutableCharacteristic 724 | else { fatalError("Invalid key") } 725 | 726 | characteristics[key] = newValue 727 | } 728 | } 729 | 730 | subscript(data characteristic: CBCharacteristic) -> Data { 731 | 732 | get { 733 | 734 | guard let key = characteristic as? CBMutableCharacteristic 735 | else { fatalError("Invalid key") } 736 | 737 | guard let cache = characteristics[key] 738 | else { fatalError("No stored characteristic matches \(characteristic)") } 739 | 740 | return cache.value 741 | } 742 | 743 | set { self[characteristic: characteristic].value = newValue } 744 | } 745 | } 746 | } 747 | 748 | #endif 749 | --------------------------------------------------------------------------------