{"id":1196,"url":"https://github.com/steamclock/bluejay","last_synced_at":"2025-04-08T09:07:20.529Z","repository":{"id":22170835,"uuid":"77253071","full_name":"steamclock/bluejay","owner":"steamclock","description":"A simple Swift framework for building reliable Bluetooth LE apps.","archived":false,"fork":false,"pushed_at":"2024-01-10T20:03:55.000Z","size":18053,"stargazers_count":1101,"open_issues_count":63,"forks_count":98,"subscribers_count":31,"default_branch":"master","last_synced_at":"2025-04-01T07:47:30.729Z","etag":null,"topics":["bluetooth","ios","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/steamclock.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-12-23T22:09:54.000Z","updated_at":"2025-03-11T09:32:57.000Z","dependencies_parsed_at":"2024-06-18T15:24:42.605Z","dependency_job_id":"1e4e75a1-33f6-42a4-9640-c7caa0fb56a7","html_url":"https://github.com/steamclock/bluejay","commit_stats":null,"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fbluejay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fbluejay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fbluejay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steamclock%2Fbluejay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/steamclock","download_url":"https://codeload.github.com/steamclock/bluejay/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247809962,"owners_count":20999816,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bluetooth","ios","swift"],"created_at":"2024-01-05T20:15:41.026Z","updated_at":"2025-04-08T09:07:20.510Z","avatar_url":"https://github.com/steamclock.png","language":"Swift","funding_links":[],"categories":["Hardware","Libs","Swift","Hardware [🔝](#readme)"],"sub_categories":["Bluetooth","Hardware","Other free courses"],"readme":"![Bluejay](https://raw.githubusercontent.com/steamclock/bluejay/master/bluejay-wordmark.png)\n\n![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Bluejay.svg)\n![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)\n[![Platform](https://img.shields.io/cocoapods/p/Bluejay.svg?style=flat)](http://cocoadocs.org/docsets/Bluejay)\n[![license](https://img.shields.io/github/license/mashape/apistatus.svg)]()\n\n\nBluejay is a simple Swift framework for building reliable Bluetooth LE apps.\n\nBluejay's primary goals are:\n- Simplify talking to a single Bluetooth LE peripheral\n- Make it easier to handle Bluetooth operations reliably\n- Take advantage of Swift features and conventions\n\n## Index\n\n- [Features](#features)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Demo](#demo)\n- [Usage](#usage)\n  - [Initialization](#initialization)\n  - [Bluetooth Events](#bluetooth-events)\n  - [Services and Characteristics](#services-and-characteristics)\n  - [Scanning](#scanning)\n  - [Connecting](#connecting)\n  - [Disconnect](#disconnect)\n  - [Cancel Everything](#cancel-everything)\n  - [Auto Reconnect](#auto-reconnect)\n  - [Disconnect Handler](#disconnect-handler)\n  - [Connection States](#connection-states)\n- [Deserialization and Serialization](#deserialization-and-serialization)\n  - [Receivable](#receivable)\n  - [Sendable](#sendable)\n- [Interactions](#interactions)\n  - [Reading](#reading)\n  - [Writing](#writing)\n  - [Listening](#listening)\n  - [Background Task](#background-task)\n- [Background Restoration](#background-restoration)\n  - [Background Permission](#background-permission)\n  - [State Restoration](#state-restoration)\n  - [Listen Restoration](#listen-restoration)\n- [Advanced Usage](#advanced-usage)\n  - [Write and Assemble](#write-and-assemble)\n  - [Flush Listen](#flush-listen)\n  - [CoreBluetooth Migration](#corebluetooth-migration)\n  - [Monitor Peripheral Services](#monitor-peripheral-services)\n\n## Features\n\n- A callback-based API\n- A FIFO operation queue for more synchronous and predictable behaviour\n- A background task mode for batch operations that avoids the \"callback pyramid of death\"\n- Simple protocols for data serialization and deserialization\n- An easy and safe way to observe connection states\n- Powerful background restoration support\n- Extended error handling and logging support\n\n## Requirements\n\n- iOS 11 or later recommended\n- Xcode 11.3.1 or later recommended\n- Swift 5 or later recommended\n\n## Installation\n\n### CocoaPods\n\n`pod 'Bluejay', '~\u003e 0.8'`\n\nOr to try the latest master:\n\n`pod 'Bluejay', :git =\u003e 'https://github.com/steamclock/bluejay.git', :branch =\u003e 'master'`\n\n### Carthage\n\n```\ngithub \"steamclock/bluejay\" ~\u003e 0.8\ngithub \"DaveWoodCom/XCGLogger\" ~\u003e 6.1.0\n```\n\nRefer to [official Carthage documentation](https://github.com/Carthage/Carthage#supporting-carthage-for-your-framework) for the rest of the instructions.\n\n**Note:** `Bluejay.framework`, `ObjcExceptionBridging.framework`, and `XCGLogger.framework` are all required.\n\n### Import\n\n```swift\nimport Bluejay\n```\n\n## Demo\n\nThe iOS Simulator does not simulate Bluetooth, and you may not have a debuggable Bluetooth LE peripheral handy, so we have prepared you a pair of demo apps to test with.\n\n1. **BluejayHeartSensorDemo:** an app that can connect to a Bluetooth LE heart sensor.\n2. **DittojayHeartSensorDemo:** a virtual Bluetooth LE heart sensor.\n\n#### To try out Bluejay:\n\n1. Get two iOS devices – one to run **Bluejay Demo**, and the other to run **Dittojay Demo**.\n2. Grant permission for notifications on **Bluejay Demo**.\n3. Grant permission for background mode on **Dittojay Demo**.\n4. Connect using **Bluejay Demo**.\n\n#### To try out background restoration (after connecting):\n\n1. In **Bluejay Demo**, tap on \"End listen to heart rate\".\n- This is to prevent the continuous heart rate notification from triggering state restoration right after we terminate the app, as it's much clearer and easier to verify state restoration when we can manually trigger a Bluetooth event at our own leisure and timing.\n2. Tap on \"Terminate app\".\n- This will crash the app, but also simulate app termination due to memory pressure, **and** allow CoreBluetooth to cache the current session and wait for Bluetooth events to begin state restoration.\n3. In **Dittojay Demo**, tap on \"Chirp\" to *revive* **Bluejay Demo**\n- This will send a Bluetooth event to the device with the terminated **Bluejay Demo**, and its CoreBluetooth stack will wake up the app in the background and execute a few quick tasks, such as scheduling a few local notifications for verification and debugging purposes in this case.\n\n## Usage\n\n### Initialization\n\nTo create an instance of Bluejay:\n\n```swift\nlet bluejay = Bluejay()\n```\n\nWhile it is convenient to create one Bluejay instance and use it everywhere, you can also create instances in specific portions of your app and tear them down after use. It's worth noting, however, that each instance of Bluejay has its own [CBCentralManager](https://developer.apple.com/documentation/corebluetooth/cbcentralmanager), which makes the multi-instance approach somewhat more complex.\n\nOnce you've created an instance, you can start running Bluejay, which will then initialize the [CoreBluetooth](https://developer.apple.com/documentation/corebluetooth) session. Note that **instantiating a Bluejay instance and running a Bluejay instance are two separate operations.**\n\nYou must always start Bluejay in your AppDelegate's `application(_:didFinishLaunchingWithOptions:)` if you want to support [background restoration](#background-restoration), otherwise you are free to start Bluejay anywhere appropriate in your app. For example, apps that don't require background restoration often initialize and start their Bluejay instance from the initial view controller.\n\n```swift\nbluejay.start()\n```\n\nIf your app needs Bluetooth to work in the background, then you have to support background restoration in your app. While Bluejay has already simplified much of background restoration for you, [it will still take some extra work](#background-restoration), and we also recommend reviewing the [relevant Apple docs](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html). Background restoration is tricky and difficult to get right.\n\nBluejay also supports [CoreBluetooth migration](#corebluetooth-migration) for working with other Bluetooth libraries or with your own Bluetooth code.\n\n### Bluetooth Events\n\nThe `ConnectionObserver` protocol allows a class to monitor and to respond to major Bluetooth and connection-related events:\n\n```swift\npublic protocol ConnectionObserver: class {\n    func bluetoothAvailable(_ available: Bool)\n    func connected(to peripheral: PeripheralIdentifier)\n    func disconnected(from peripheral: PeripheralIdentifier)\n}\n```\n\nYou can register a connection observer using:\n\n```swift\nbluejay.register(connectionObserver: batteryLabel)\n```\n\nUnregistering a connection observer is not necessary, because Bluejay only holds weak references to registered observers, so Bluejay will clear nil observers from its list when they are found at the next event's firing. But if you need to do so before that happens, you can use:\n\n```swift\nbluejay.unregister(connectionObserver: rssiLabel)\n```\n\n### Services and Characteristics\n\nIn Bluetooth parlance, a Service is a group of attributes, and a Characteristic is an attribute belonging to a group. For example, BLE peripherals that can detect heart rates typically have a Service named \"Heart Rate\" with a UUID of \"180D\". Inside that Service are Characteristics such as \"Body Sensor Location\" with a UUID of \"2A38\", as well as \"Heart Rate Measurement\" with a UUID of \"2A37\".\n\nMany of these Services and Characteristics are standards specified by the Bluetooth SIG organization, and most hardware adopt their specifications. For example, most BLE peripherals implement the Service \"Device Information\" which has a UUID of \"180A\", which is where Characteristics such as firmware version, serial number, and other hardware details can be found. Of course, there are many BLE uses not covered by the Bluetooth Core Spec, and custom hardware often have their own unique Services and Characteristics.\n\nHere is how you can specify Services and Characteristics for use in Bluejay:\n\n```swift\nlet heartRateService = ServiceIdentifier(uuid: \"180D\")\nlet bodySensorLocation = CharacteristicIdentifier(uuid: \"2A38\", service: heartRateService)\nlet heartRate = CharacteristicIdentifier(uuid: \"2A37\", service: heartRateService)\n```\n\nBluejay uses the `ServiceIdentifier` and `CharacteristicIdentifier` structs to avoid problems like accidentally specifying a Service when a Characteristic is expected.\n\n### Scanning\n\nBluejay has a powerful scanning API that can be be used simply or customized to satisfy many use cases.\n\nCoreBluetooth scans for devices using services. In other words, CoreBluetooth, and therefore Bluejay, expects you to know beforehand one or several public services the peripherals you want to scan for contains.\n\n#### Basic Scanning\n\nThis simple call will just notify you when there is a new discovery, and when the scan has finished:\n\n```swift\nbluejay.scan(\n    serviceIdentifiers: [heartRateService],\n    discovery: { [weak self] (discovery, discoveries) -\u003e ScanAction in\n        guard let weakSelf = self else {\n            return .stop\n        }\n\n        weakSelf.discoveries = discoveries\n        weakSelf.tableView.reloadData()\n\n        return .continue\n    },\n    stopped: { (discoveries, error) in\n        if let error = error {\n            debugPrint(\"Scan stopped with error: \\(error.localizedDescription)\")\n        }\n        else {\n            debugPrint(\"Scan stopped without error.\")\n        }\n})\n```\n\nA scan result `(ScanDiscovery, [ScanDiscovery])` contains the current discovery followed by an array of all the discoveries made so far.\n\nThe stopped result contains a final list of discoveries available just before stopping, and an error if there is one. If there isn't an error, that means that the scan was stopped intentionally or expectedly.\n\n#### Scan Action\n\nA `ScanAction` is returned at the end of a discovery callback to tell Bluejay whether to keep scanning or to stop.\n\n```swift\npublic enum ScanAction {\n    case `continue`\n    case blacklist\n    case stop\n    case connect(ScanDiscovery, (ConnectionResult) -\u003e Void)\n}\n```\n\nReturning `blacklist` will ignore any future discovery of the same peripheral within the current scan session. This is only useful when `allowDuplicates` is set to true. See [Apple docs on CBCentralManagerScanOptionAllowDuplicatesKey](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerscanoptionallowduplicateskey?language=objc) for more info.\n\nReturning `connect` will make Bluejay stop the scan as well as perform your connection request. This is useful if you want to connect right away when you've found the peripheral you're looking for.\n\n**Tip:** You can set up the `ConnectionResult` block outside the scan call to reduce callback nesting.\n\n#### Monitoring\n\nAnother useful way to use the scanning API is to scan continuously, i.e. to monitor, for purposes such as observing the RSSI changes of nearby peripherals to estimate their proximity:\n\n```swift\nbluejay.scan(\n    duration: 15,\n    allowDuplicates: true,\n    serviceIdentifiers: nil,\n    discovery: { [weak self] (discovery, discoveries) -\u003e ScanAction in\n        guard let weakSelf = self else {\n            return .stop\n        }\n\n        weakSelf.discoveries = discoveries\n        weakSelf.tableView.reloadData()\n\n        return .continue\n    },\n    expired: { [weak self] (lostDiscovery, discoveries) -\u003e ScanAction in\n        guard let weakSelf = self else {\n            return .stop\n        }\n\n        debugPrint(\"Lost discovery: \\(lostDiscovery)\")\n\n        weakSelf.discoveries = discoveries\n        weakSelf.tableView.reloadData()\n\n        return .continue\n}) { (discoveries, error) in\n        if let error = error {\n            debugPrint(\"Scan stopped with error: \\(error.localizedDescription)\")\n        }\n        else {\n            debugPrint(\"Scan stopped without error.\")\n        }\n}\n```\n\nSetting `allowDuplicates` to true will stop coalescing multiple discoveries of the same peripheral into one single discovery callback. Instead, you'll get a discovery call every time a peripheral's advertising packet is picked up. This will **consume more battery, and does not work in the background**.\n\n**Warning:** An allow duplicates scan will stop with an error if your app is backgrounded during the scan.\n\nThe `expired` callback is only invoked when `allowDuplicates` is true. This is called when Bluejay estimates that a previously discovered peripheral is likely out of range or no longer broadcasting. Essentially, when `allowDuplicates` is set to true, every time a peripheral is discovered a timer associated with that peripheral starts counting down. If that peripheral is within range, and even if it has a slow broadcasting interval, it is likely that peripheral will be picked up by the scan again and cause the timer to refresh. If not and the timer expires without being refreshed, Bluejay makes an educated guess and suggests that the peripheral is no longer reachable. Be aware that this is an estimation.\n\n**Warning**: Setting `serviceIdentifiers` to `nil` will result in picking up all available Bluetooth peripherals in the vicinity, **but is not recommended by Apple**. It may cause **battery and cpu issues** on prolonged scanning, and it also **doesn't work in the background**. It is not a private API call, but an available option where you need a quick solution when testing and prototyping.\n\n**Tip:** Specifying at least one specific service identifier is the most common way to scan for Bluetooth devices in iOS. If you need to scan for all Bluetooth devices, we recommend making use of the `duration` parameter to stop the scan after 5 ~ 10 seconds to avoid scanning indefinitely and overloading the hardware.\n\n### Connecting\n\nIt is important to keep in mind that Bluejay is designed to work with a single BLE peripheral. Multiple connections at once is not currently supported, and a connection request will fail if Bluejay is already connected or is still connecting. Although this can be a limitation for some sophisticated apps, it is more commonly a safeguard to ensure your app does not issue connections unnecessarily or erroneously.\n\n```swift\nbluejay.connect(selectedSensor, timeout: .seconds(15)) { result in\n    switch result {\n    case .success:\n        debugPrint(\"Connection attempt to: \\(selectedSensor.description) is successful\")\n    case .failure(let error):\n        debugPrint(\"Failed to connect with error: \\(error.localizedDescription)\")\n    }\n}\n```\n\n#### Timeouts\n\nYou can also specify a timeout for a connection request, default is no timeout:\n\n```swift\npublic enum Timeout {\n    case seconds(TimeInterval)\n    case none\n}\n```\n\n**Tip:** We recommend always setting at least a 15 seconds timeout for your connection requests.\n\n### Disconnect\n\nTo disconnect:\n\n```swift\nbluejay.disconnect()\n```\n\nBluejay also supports finer controls over your disconnection:\n\n#### Queued Disconnect\n\nA queued disconnect will be queued like all other Bluejay API requests, so the disconnect attempt will wait for its turn until all the queued tasks are finished.\n\nTo perform a queued disconnect, simply call:\n\n```swift\nbluejay.disconnect()\n```\n\n#### Immediate Disconnect\n\nAn immediate disconnect will immediately fail and empty all tasks from the queue even if they are still running and then immediately disconnect.\n\nThere are two ways to perform an immediate disconnect:\n\n```swift\nbluejay.disconnect(immediate: true)\n```\n\n```swift\nbluejay.cancelEverything()\n```\n\n#### Expected vs Unexpected Disconnection\n\nBluejay's log will describe in detail whether a disconnection is expected or unexpected. This is important when debugging a disconnect-related issue, as well as explaining why Bluejay is or isn't attempting to auto reconnect.\n\nAny explicit call to `disconnect` or `cancelEverything` with disconnect will result in an expected disconnection.\n\nAll other disconnection events will be considered unexpected. For examples:\n- If a connection attempt fails due to hardware errors and not from a timeout\n- If a connected device moves out of range\n- If a connected device runs out of battery or is shut off\n- If a connected device's Bluetooth module crashes and is no longer negotiable\n\n### Cancel Everything\n\nThe reason why there is a `cancelEverything` API in addition to `disconnect`, is because sometimes we want to cancel everything in the queue but **remain** connected.\n\n```swift\nbluejay.cancelEverything(shouldDisconnect: false)\n```\n\n### Auto Reconnect\n\nBy default, `shouldAutoReconnect` is `true` and Bluejay will always try to automatically reconnect after an unexpected disconnection.\n\nBluejay will only set `shouldAutoReconnect` to `false` under these circumstances:\n\n1. If you manually call `disconnect` and the disconnection is successful.\n2. If you manually call `cancelEverything` and its disconnection is successful.\n\nBluejay will also **always** reset `shouldAutoReconnect` to `true` on a successful connection to a peripheral, as we usually want to reconnect to the same device as soon as possible if a connection is lost unexpectedly during normal usage.\n\nHowever, there are some cases where auto reconnect is not desirable. In those cases, use a `DisconnectHandler` to evaluate and to override auto reconnect.\n\n### Disconnect Handler\n\nA disconnect handler is a single delegate that is suitable for performing major recovery, retry, or reset operations, such as restarting a scan when there is a disconnection.\n\nThe purpose of this handler is to help avoid writing and repeating major resuscitation and error handling logic inside the error callbacks of your regular connect, disconnect, read, write, and listen calls. Use the disconnect handler to perform one-time and significant operations at the very end of a disconnection.\n\nIn addition to helping you avoid redundant and conflicted logic in various callbacks when there is a disconnection, the disconnect handler also allows you to evaluate and to control Bluejay's auto-reconnect behaviour.\n\nFor example, this protocol implementation will always turn off auto reconnect whenever there is a disconnection, expected or not.\n\n```swift\nfunc didDisconnect(\n  from peripheral: PeripheralIdentifier,\n  with error: Error?,\n  willReconnect autoReconnect: Bool) -\u003e AutoReconnectMode {\n    return .change(shouldAutoReconnect: false)\n}\n```\n\nWe also anticipate that for most apps, different view controllers may want to handle disconnection differently, so simply register and replace the existing disconnect handler as your user navigates to different parts of your app.\n\n```swift\nbluejay.registerDisconnectHandler(handler: self)\n```\n\nSimilar to connection observers, you do not have to explicitly unregister unless you need to.\n\n### Connection States\n\nYour Bluejay instance has these properties to help you make connection-related decisions:\n\n- `isBluetoothAvailable`\n- `isBluetoothStateUpdateImminent`\n- `isConnecting`\n- `isConnected`\n- `isDisconnecting`\n- `shouldAutoReconnect`\n- `isScanning`\n- `hasStarted`\n- `defaultWarningOptions`\n- `isBackgroundRestorationEnabled`\n\n## Deserialization and Serialization\n\nReading, writing, and listening to Characteristics is straightforward in Bluejay. Most of the work involved is building out the deserialization and serialization for your data. Let's have a quick look at how Bluejay helps standardize this process in your app via the `Receivable` and `Sendable` protocols.\n\n#### Receivable\n\nModels that represent data you wish to read and receive from your peripheral should all conform to the `Receivable` protocol.\n\nHere is a partial example for the [Heart Rate Measurement Characteristic](https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml):\n\n```swift\nimport Bluejay\nimport Foundation\n\nstruct HeartRateMeasurement: Receivable {\n\n    private var flags: UInt8 = 0\n    private var measurement8bits: UInt8 = 0\n    private var measurement16bits: UInt16 = 0\n    private var energyExpended: UInt16 = 0\n    private var rrInterval: UInt16 = 0\n\n    private var isMeasurementIn8bits = true\n\n    var measurement: Int {\n        return isMeasurementIn8bits ? Int(measurement8bits) : Int(measurement16bits)\n    }\n\n    init(bluetoothData: Data) throws {\n        flags = try bluetoothData.extract(start: 0, length: 1)\n\n        isMeasurementIn8bits = (flags \u0026 0b00000001) == 0b00000000\n\n        if isMeasurementIn8bits {\n            measurement8bits = try bluetoothData.extract(start: 1, length: 1)\n        } else {\n            measurement16bits = try bluetoothData.extract(start: 1, length: 2)\n        }\n    }\n\n}\n\n```\n\nNote how you can use the `extract` function that Bluejay adds to `Data` to easily parse the bytes you need. We have plans to build more protection and error handling for this in the future.\n\nFinally, while it is not essential and it will depend on the context, we suggest only exposing the needed and computed properties of your models.\n\n#### Sendable\n\nModels representing data you wish to send to your peripheral should all conform to the `Sendable` protocol. In a nutshell, this is how you help Bluejay determine how to convert your models into `Data`:\n\n```swift\nimport Foundation\nimport Bluejay\n\nstruct Coffee: Sendable {\n\n    let data: UInt8\n\n    init(coffee: CoffeeEnum) {\n        data = UInt8(coffee.rawValue)\n    }\n\n    func toBluetoothData() -\u003e Data {\n        return Bluejay.combine(sendables: [data])\n    }\n\n}\n```\n\nThe `combine` helper function makes it easier to group and to sequence the outgoing data.\n\n#### Sending and Receiving Primitives\n\nIn some cases, you may want to send or receive data simple enough that creating a custom struct which implements `Sendable` or `Receivable` to hold it is unnecessarily complicated. For those cases, Bluejay also retroactively conforms several built-in Swift types to `Sendable` and `Receivable`. `Int8`, `Int16`, `Int32`, `Int64`, `UInt8`, `UInt16`, `UInt32`, `UInt64`, `Data` are all conformed to both protocols and so they can all be sent or received directly.\n\n`Int` and `UInt` are intentionally not conformed. Bluetooth values are always sent and/or received at a specific bit width. The intended bit width for an `Int` is ambiguous, and trying to use one often indicates a programmer error, in the form of not considering the bit width the Bluetooth device is expecting on a characteristic.\n\n## Interactions\n\nOnce you have your data modelled using either the `Receivable` or `Sendable` protocol, the read, write, and listen APIs in Bluejay should handle the deserialization and serialization seamlessly for you. All you need to do is to specify the type for the generic result wrappers: `ReadResult\u003cT\u003e` or `WriteResult\u003cT\u003e`.\n\n### Reading\n\nHere is an example showing how to read the [sensor body location characteristic](https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml), and converting its value to its corresponding string and display it in the UI.\n\n```swift\nlet heartRateService = ServiceIdentifier(uuid: \"180D\")\nlet sensorLocation = CharacteristicIdentifier(uuid: \"2A38\", service: heartRateService)\n\nbluejay.read(from: sensorLocation) { [weak self] (result: ReadResult\u003cUInt8\u003e) in\n    guard let weakSelf = self else {\n\t     return\n    }\n\n    switch result {\n    case .success(let location):\n        debugPrint(\"Read from sensor location is successful: \\(location)\")\n\n        var locationString = \"Unknown\"\n\n        switch location {\n        case 0:\n            locationString = \"Other\"\n        case 1:\n            locationString = \"Chest\"\n        case 2:\n            locationString = \"Wrist\"\n        case 3:\n            locationString = \"Finger\"\n        case 4:\n            locationString = \"Hand\"\n        case 5:\n            locationString = \"Ear Lobe\"\n        case 6:\n            locationString = \"Foot\"\n        default:\n            locationString = \"Unknown\"\n        }\n\n        weakSelf.sensorLocationCell.detailTextLabel?.text = locationString\n    case .failure(let error):\n        debugPrint(\"Failed to read sensor location with error: \\(error.localizedDescription)\")\n    }\n}\n```\n\n### Writing\n\nWriting to a characteristic is very similar to reading:\n\n```swift\nlet heartRateService = ServiceIdentifier(uuid: \"180D\")\nlet sensorLocation = CharacteristicIdentifier(uuid: \"2A38\", service: heartRateService)\n\nbluejay.write(to: sensorLocation, value: UInt8(2)) { result in\n    switch result {\n    case .success:\n        debugPrint(\"Write to sensor location is successful.\")\n    case .failure(let error):\n        debugPrint(\"Failed to write sensor location with error: \\(error.localizedDescription)\")\n    }\n}\n```\n\n### Listening\n\nListening turns on broadcasting on a characteristic and allows you to receive its notifications.\n\nUnlike read and write where the completion block is only called once, listen callbacks are persistent. It could be minutes (or never) before the receive block is called, and the block can be called multiple times.\n\nSome Bluetooth devices will turn off notifications when it is disconnected, some don't. That said, when you don't need to listen anymore, it is generally good practice to always explicitly turn off broadcasting on that characteristic using the `endListen` function.\n\nNot all characteristics support listening, it is a feature that must be enabled for a characteristic on the Bluetooth device itself.\n\n```swift\nlet heartRateService = ServiceIdentifier(uuid: \"180D\")\nlet heartRateCharacteristic = CharacteristicIdentifier(uuid: \"2A37\", service: heartRateService)\n\nbluejay.listen(to: heartRateCharacteristic, multipleListenOption: .replaceable)\n{ [weak self] (result: ReadResult\u003cHeartRateMeasurement\u003e) in\n        guard let weakSelf = self else {\n            return\n        }\n\n        switch result {\n        case .success(let heartRate):\n            weakSelf.heartRate = heartRate\n            weakSelf.tableView.reloadData()\n        case .failure(let error):\n            debugPrint(\"Failed to listen with error: \\(error.localizedDescription)\")\n        }\n}\n```\n\n#### Multiple Listen Options\n\nYou can only have one listener callback installed per characteristic. If you need multiple observers on the same characteristic, you can still do so yourself using just one Bluejay listener and within it create your own app-specific notifications.\n\nPass in the appropriate `MultipleListenOption` in your listen call to either protect against multiple listen attempts on the same characteristic, or to intentionally allow overwriting an existing listen.\n\n```swift\n/// Ways to handle calling listen on the same characteristic multiple times.\npublic enum MultipleListenOption: Int {\n    /// New listen on the same characteristic will not overwrite an existing listen.\n    case trap\n    /// New listens on the same characteristic will replace the existing listen.\n    case replaceable\n}\n```\n\n### Background Task\n\nBluejay also supports performing a longer series of reads, writes, and listens in a background thread. Each operation in a background task is blocking and will not return until completed.\n\nThis is useful when you need to complete a specific and large task such as syncing or upgrading to a new firmware. This is also useful when working with a notification-based Bluetooth module where you need to pause and wait for Bluetooth execution, primarily the listen operation, but without blocking the main thread.\n\nBluejay will call your completion block on the main thread when everything finishes without an error, or if any one of the operations in the background task has failed.\n\nHere's a made-up example in trying get both user and admin access to a Bluetooth device using the same password:\n\n```swift\nvar isUserAuthenticated = false\nvar isAdminAuthenticated = false\n\nbluejay.run(backgroundTask: { (peripheral) -\u003e (Bool, Bool) in\n    // 1. No need to perform any Bluetooth tasks if there's no password to try.\n    guard let password = enteredPassword else {\n      return (false, false)\n    }\n\n    // 2. Flush auth characteristics in case they are still broadcasting unwanted data.\n    try peripheral.flushListen(to: userAuth, nonZeroTimeout: .seconds(3), completion: {\n        debugPrint(\"Flushed buffered data on the user auth characteristic.\")\n    })\n\n    try peripheral.flushListen(to: adminAuth, nonZeroTimeout: .seconds(3), completion: {\n        debugPrint(\"Flushed buffered data on the admin auth characteristic.\")\n    })\n\n    // 3. Sanity checks, making sure the characteristics are not broadcasting anymore.\n    try peripheral.endListen(to: userAuth)\n    try peripheral.endListen(to: adminAuth)\n\n    // 4. Attempt authentication.\n    if let passwordData = password.data(using: .utf8) {\n        debugPrint(\"Begin authentication...\")\n\n        try peripheral.writeAndListen(\n            writeTo: userAuth,\n            value: passwordData,\n            listenTo: userAuth,\n            timeoutInSeconds: .seconds(15),\n            completion: { (response: UInt8) -\u003e ListenAction in\n                if let responseCode = AuthResponse(rawValue: response) {\n                    isUserAuthenticated = responseCode == .success\n                }\n\n                return .done\n        })\n\n        try peripheral.writeAndListen(\n            writeTo: adminAuth,\n            value: passwordData,\n            listenTo: adminAuth,\n            timeoutInSeconds: .seconds(15),\n            completion: { (response: UInt8) -\u003e ListenAction in\n                if let responseCode = AuthResponse(rawValue: response) {\n                    isAdminAuthenticated = responseCode == .success\n                }\n\n                return .done\n        })\n    }\n\n    // 5. Return results of authentication.\n    return (isUserAuthenticated, isAdminAuthenticated)\n}, completionOnMainThread: { (result) in\n    switch result {\n    case .success(let authResults):\n        debugPrint(\"Is user authenticated: \\(authResults.0)\")\n        debugPrint(\"Is admin authenticated: \\(authResults.1)\")\n    case .failure(let error):\n        debugPrint(\"Background task failed with error: \\(error.localizedDescription)\")\n    }\n})\n```\n\n**Important:**\n\nWhile Bluejay will not crash because it has built in error handling that will inform you of the following violations, these rules are are still worth calling out:\n\n1. **Do not** call any regular `read/write/listen` functions inside the `backgroundTask` block. Use the `SynchronizedPeripheral` provided to you and its `read/write/listen` API instead.\n2. Regular `read/write/listen` calls outside of the `backgroundTask` block will **also not work** when a background task is still running.\n\nNote that because the `backgroundTask` block is running on a background thread, you need to be careful about accessing any global or captured data inside that block for thread safety reasons, like you would with any GCD or OperationQueue task. To help with this, use `run(userData:backgroundTask:completionOnMainThread:)` to pass an object you wish to have thread-safe access to while working inside the background task.\n\n## Background Restoration\n\n[CoreBluetooth allows apps to continue processing active Bluetooth operations when it is backgrounded or even when it is evicted from memory](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html). In Bluejay, we refer to this feature and behaviour as \"background restoration\". For examples, a pending connect request that finishes, or a subscribed characteristic that fires a notification, can cause the system to wake or restart the app in the background. This can, for example, allow syncing data from a device without requiring the user to launch the app.\n\nIn order to support background Bluetooth, there are two steps to take:\n1. Give your app permission to use Bluetooth in the background\n2. Implement and handle state restoration\n\n### Background Permission\n\nThis is the easy step. Just turn on the **Background Modes** capability in your Xcode project with **Uses Bluetooth LE accessories** enabled.\n\n### State Restoration\n\nBluejay already handles much of the gnarly state restoration implementation for you. However, there are still a few things you need to do to help Bluejay help you:\n\n1. Create a background restoration configuration with a restore identifier\n2. Always start your Bluejay instance in your AppDelegate's `application(_:didFinishLaunchingWithOptions:)`\n3. Always pass Bluejay the `launchOptions`\n4. Setup a `BackgroundRestorer` and a `ListenRestorer` to handle restoration results\n\n```swift\nimport Bluejay\nimport UIKit\n\nlet bluejay = Bluejay()\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    var window: UIWindow?\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -\u003e Bool {\n        // Override point for customization after application launch.\n        let backgroundRestoreConfig = BackgroundRestoreConfig(\n            restoreIdentifier: \"com.steamclock.bluejayHeartSensorDemo\",\n            backgroundRestorer: self,\n            listenRestorer: self,\n            launchOptions: launchOptions)\n\n        let backgroundRestoreMode = BackgroundRestoreMode.enable(backgroundRestoreConfig)\n\n        let options = StartOptions(\n          enableBluetoothAlert: true,\n          backgroundRestore: backgroundRestoreMode)\n\n        bluejay.start(mode: .new(options))\n\n        return true\n    }\n\n}\n\nextension AppDelegate: BackgroundRestorer {\n    func didRestoreConnection(\n      to peripheral: PeripheralIdentifier) -\u003e BackgroundRestoreCompletion {\n        // Opportunity to perform syncing related logic here.\n        return .continue\n    }\n\n    func didFailToRestoreConnection(\n      to peripheral: PeripheralIdentifier, error: Error) -\u003e BackgroundRestoreCompletion {\n        // Opportunity to perform cleanup or error handling logic here.\n        return .continue\n    }\n}\n\nextension AppDelegate: ListenRestorer {\n    func didReceiveUnhandledListen(\n      from peripheral: PeripheralIdentifier,\n      on characteristic: CharacteristicIdentifier,\n      with value: Data?) -\u003e ListenRestoreAction {\n        // Re-install or defer installing a callback to a notifying characteristic.\n        return .promiseRestoration\n    }\n}\n```\n\nWhile Bluejay has simplified background restoration to just a few initialization rules and two protocols, it can still be difficult to get right. Please contact us if you have any questions\n\n### Listen Restoration\n\nIf you app is evicted from memory, you lose all your listen callbacks as well. Yet, the Bluetooth device can still be broadcasting on the characteristics you were listening to. Listen restoration gives you an opportunity to restore and to respond to that notification when your app is restored in the background.\n\nIf you need to re-install a listen, simply call `listen` again as you normally would when setting up a new listen inside `didReceiveUnhandledListen(from:on:with:)` before returning `.promiseRestoration`. Otherwise, return `.stopListen` to ask Bluejay to turn off notification on that characteristic.\n\n```swift\n/**\n * Available actions to take on an unhandled listen event from background restoration.\n */\npublic enum ListenRestoreAction {\n    /// Bluejay will continue to receive but do nothing with the incoming listen events until a new listener is installed.\n    case promiseRestoration\n    /// Bluejay will attempt to turn off notifications on the peripheral.\n    case stopListen\n}\n```\n\n```swift\nextension AppDelegate: ListenRestorer {\n    func didReceiveUnhandledListen(\n      from peripheral: PeripheralIdentifier,\n      on characteristic: CharacteristicIdentifier,\n      with value: Data?) -\u003e ListenRestoreAction {\n        // Re-install or defer installing a callback to a notifying characteristic.\n        return .promiseRestoration\n    }\n}\n```\n\n## Advanced Usage\n\nThe following section will demonstrate a few advanced usage of Bluejay.\n\n### Write and Assemble\n\nOne of the Bluetooth modules we've worked with doesn't always send back the entire data in one packet, even if the data is smaller than either the software's or hardware's maximum packet size. To handle incoming data that can be broken up into an unknown number of packets, we've added the `writeAndAssemble` function that is very similar to `writeAndListen` on the `SynchronizedPeripheral`. Therefore, at least for now, this is currently only supported when using the [background task](#background-task).\n\nWhen using `writeAndAssemble`, we still expect you to know the total size of the data you are receiving, but Bluejay will keep listening and receiving packets until the expected size is reached before trying to [deserialize](#deserialization-and-serialization) the data into the object you need.\n\nYou can also specify a timeout in case something hangs or takes abnormally long.\n\nHere is an example writing a request for a value to a Bluetooth module, so that it can return the value we want via a notification on a characteristic. And of course, we're not sure and have no control over how many packets the module will send back.\n\n```swift\ntry peripheral.writeAndAssemble(\n    writeTo: Characteristics.rigadoTX,\n    value: ReadRequest(handle: Registers.system.firmwareVersion),\n    listenTo: Characteristics.rigadoRX,\n    expectedLength: FirmwareVersion.length,\n    completion: { (firmwareVersion: FirmwareVersion) -\u003e ListenAction in\n        settings.firmware = firmwareVersion.string\n        return .done\n})\n```\n\n### Flush Listen\n\nSome Bluetooth modules will pause sending data when it loses connection to your app, then resume sending the same set of data from where it left off when the connection is re-established. This isn't an issue most of the time, except for Bluetooth modules that do overload one characteristic with multiple purposes and values.\n\nFor example, you might have to re-authenticate the user when the app is re-opened. But if authentication requires listening to the same characteristic where an incomplete data set from a previous request is still being sent, then you will be getting back unexpected values and most likely crash when trying to [deserialize](#deserialization-and-serialization) authentication related objects.\n\nTo handle this, it is often a good idea to flush a notifiable characteristic before starting a critical operation. This is also only available on the `SynchronizedPeripheral` when working within the [background task](#background-task)\n\n```swift\ntry peripheral.flushListen(to: auth, nonZeroTimeout: .seconds(3), completion: {\n    debugPrint(\"Flushed buffered data on the auth characteristic.\")\n})\n```\n\nThe `nonZeroTimeout` specifies the duration of the **absence of incoming data** needed to predict that the flush is most likely completed. In the above example, it is not that the flush will come to a hard stop after 3 seconds, but rather will only stop if Bluejay doesn't have any data to flush after waiting for 3 seconds. It will continue to flush for as long as there is incoming data.\n\n### CoreBluetooth Migration\n\nIf you want to start Bluejay with a pre-existing CoreBluetooth stack, you can do so by specifying `.use` in the start mode instead of `.new` when calling the `start` function.\n\n```swift\nbluejay.start(mode: .use(manager: anotherManager, peripheral: alreadyConnectedPeripheral))\n```\n\nYou can also transfer Bluejay's CoreBluetooth stack to another Bluetooth library or your own using this function:\n\n```swift\npublic func stopAndExtractBluetoothState() -\u003e\n    (manager: CBCentralManager, peripheral: CBPeripheral?)\n```\n\nFinally, you can check whether Bluejay has been started or stopped using the `hasStarted` property.\n\n### Monitor Peripheral Services\n\nSome peripherals can add or remove services while it's being used, and Bluejay provides a basic way to react to this. See **BluejayHeartSensorDemo** and **DittojayHeartSensorDemo** in the project for more examples.\n\n```swift\nbluejay.register(serviceObserver: self)\n```\n\n```swift\nfunc didModifyServices(\n  from peripheral: PeripheralIdentifier,\n  invalidatedServices: [ServiceIdentifier]) {\n    if invalidatedServices.contains(where: { invalidatedServiceIdentifier -\u003e Bool in\n        invalidatedServiceIdentifier == chirpCharacteristic.service\n    }) {\n        endListen(to: chirpCharacteristic)\n    } else if invalidatedServices.isEmpty {\n        listen(to: chirpCharacteristic)\n    }\n}\n```\n\n**Notes from Apple:**\n\n\u003e If you previously discovered any of the services that have changed, they are provided in the invalidatedServices parameter and can no longer be used. You can use the discoverServices: method to discover any new services that have been added to the peripheral’s database or to find out whether any of the invalidated services that you were using (and want to continue using) have been added back to a different location in the peripheral’s database.\n\n## API Documentation\n\nWe have more [in-depth API documentation for Bluejay](https://steamclock.github.io/bluejay/index.html) using inline documentation and Jazzy.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteamclock%2Fbluejay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsteamclock%2Fbluejay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteamclock%2Fbluejay/lists"}