Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce DSL approach to model BL GATT devices #9

Merged
merged 34 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
172b4d1
Some initial progress on new client model
Supereg Jan 7, 2024
eda5a39
Some more progress
Supereg Jan 8, 2024
156d237
Initial stale timer approach, kvo observer abstraction, more modeling…
Supereg Jan 12, 2024
7a7f4a3
Use generalized lazy view approach to ensure BL manager popuop arrive…
Supereg Jan 12, 2024
b700cf1
Fix tests and REUSE
Supereg Jan 12, 2024
cfa2c73
Another license file
Supereg Jan 12, 2024
93de91d
Implement initial nearby devices search test view. Fixed a crash
Supereg Jan 12, 2024
5cdc374
Make more sophisticated stale timer infrastructure
Supereg Jan 16, 2024
960fec5
Expose CB API for service and characteristic models
Supereg Jan 16, 2024
9f6c183
Refine Peripheral model and start DSL semantic model builder
Supereg Jan 17, 2024
7a1e7c5
Updated docs, separate delegate and bunch other
Supereg Jan 17, 2024
918ed8c
Docs, initial DSL approach, just commiting current progress
Supereg Jan 17, 2024
0100f0b
Bunch of progress on the DSL and overal project structure
Supereg Jan 18, 2024
2ce2f81
Implement @Characteristic state handling
Supereg Jan 18, 2024
8344576
Fix small compiler error
Supereg Jan 18, 2024
985826c
Inject active/connected devices into the environment
Supereg Jan 18, 2024
d0bc3c5
Implement autoconnect and allow scan modifier with Bluetooth module
Supereg Jan 19, 2024
fd98c00
Minor changes
Supereg Jan 19, 2024
f2d0a15
Ensure mutation is tracked
Supereg Jan 19, 2024
70fb108
Actually hook up observation tracking. Upsie.
Supereg Jan 19, 2024
c871e75
Track state for each peripheral to update active connected device wit…
Supereg Jan 19, 2024
e6efadf
Fix use after free, fix state observation
Supereg Jan 19, 2024
4e23ff7
Fixed a bunch of state issues
Supereg Jan 19, 2024
59d2802
Initial restructuring
Supereg Jan 20, 2024
4fb2c81
A bunch more restructuring. Getting there slowely
Supereg Jan 20, 2024
f65851c
Resolve some more todos
Supereg Jan 20, 2024
c2d033f
Initital documentation catalog structure
Supereg Jan 21, 2024
a1ab546
Lazily initialize central manager. Free it once it is unused.
Supereg Jan 21, 2024
8d1738b
Auto connect modifier. Improved state handling.
Supereg Jan 21, 2024
6113cf3
Last few todos. Get started with documentation.
Supereg Jan 21, 2024
148b2b3
Complete all type documentation and add minimal UI testing
Supereg Jan 22, 2024
14a1c72
Update landing page and README
Supereg Jan 22, 2024
ac9a9e3
Minor grammar
Supereg Jan 22, 2024
a90cfbe
Remove todo
Supereg Jan 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziBluetooth
artifactname: SpeziBluetooth.xcresult
scheme: SpeziBluetooth-Package
artifactname: SpeziBluetooth-Package.xcresult
ios:
name: Build and Test iOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand All @@ -36,4 +36,4 @@ jobs:
needs: [packageios, ios]
uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
with:
coveragereports: SpeziBluetooth.xcresult TestApp.xcresult
coveragereports: SpeziBluetooth-Package.xcresult TestApp.xcresult
3 changes: 0 additions & 3 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,6 @@ only_rules:
# The variable should be placed on the left, the constant on the right of a comparison operator.
- yoda_condition

attributes:
attributes_with_arguments_always_on_line_above: false

deployment_target: # Availability checks or attributes shouldn’t be using older versions that are satisfied by the deployment target.
iOSApplicationExtension_deployment_target: 16.0
iOS_deployment_target: 16.0
Expand Down
21 changes: 16 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,39 @@ let package = Package(
.iOS(.v17)
],
products: [
.library(name: "SpeziBluetooth", targets: ["SpeziBluetooth"])
.library(name: "SpeziBluetooth", targets: ["SpeziBluetooth"]),
.library(name: "XCTBluetooth", targets: ["XCTBluetooth"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.59.0")
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.59.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4")
],
targets: [
.target(
name: "SpeziBluetooth",
dependencies: [
.product(name: "Spezi", package: "Spezi"),
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio")
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "OrderedCollections", package: "swift-collections")
],
resources: [
.process("Resources")
]
),
.target(
name: "XCTBluetooth",
dependencies: [
.target(name: "SpeziBluetooth")
]
),
.testTarget(
name: "SpeziBluetoothTests",
dependencies: [
.target(name: "SpeziBluetooth")
.target(name: "SpeziBluetooth"),
.target(name: "XCTBluetooth"),
.product(name: "NIO", package: "swift-nio")
]
)
]
Expand Down
210 changes: 113 additions & 97 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,38 @@ SPDX-License-Identifier: MIT
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziBluetooth%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziBluetooth)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziBluetooth%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziBluetooth)

Connect and communicate with Bluetooth devices.
Connect and communicate with Bluetooth devices using modern programming paradigms.


## Overview

The Spezi Bluetooth module provides a convenient way to handle state management with a Bluetooth device, retrieve data from different services and characteristics, and write data to a combination of services and characteristics.
The Spezi Bluetooth module provides a convenient way to handle state management with a Bluetooth device,
retrieve data from different services and characteristics,
and write data to a combination of services and characteristics.

This package uses Apples [CoreBluetooth](https://developer.apple.com/documentation/corebluetooth) framework under the hood.

> [!NOTE]
> You will need a basic understanding of the Bluetooth Terminology and the underlying software model to understand the structure and API of the Spezi Bluetooth module. You can find a good overview in the [Wikipedia Bluetooth Low Energy (LE) Software Model section](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy#Software_model) or the [Developer’s Guide
to Bluetooth Technology](https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth/).
> You will need a basic understanding of the Bluetooth Terminology and the underlying software model to
understand the structure and API of the Spezi Bluetooth module. You can find a good overview in the
[Wikipedia Bluetooth Low Energy (LE) Software Model section](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy#Software_model)
or the [Developer’s Guide to Bluetooth Technology](https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth/).


## Setup


### 1. Add Spezi Bluetooth as a Dependency
### Add Spezi Bluetooth as a Dependency

You need to add the Spezi Bluetooth Swift package to
[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or
[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package).

> [!IMPORTANT]
> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to setup the core Spezi infrastructure.
> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to set up the core Spezi infrastructure.


### 2. Register the Module
### Register the Module

The [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth) module needs to be registered in a Spezi-based application using the
[`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) in a
Expand All @@ -50,8 +56,9 @@ The [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/doc
class ExampleAppDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration {
Bluetooth(services: [/* ... */])
// ...
Bluetooth {
// discover devices ...
}
}
}
}
Expand All @@ -63,118 +70,127 @@ class ExampleAppDelegate: SpeziAppDelegate {

## Example

`MyDeviceModule` demonstrates the capabilities of the Spezi Bluetooth module.
This class integrates the [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth) module to create a `MyDevice` instance injected in the SwiftUI environment to send string messages over Bluetooth and collect them in a messages array.
### Create your Bluetooth device

> [!NOTE]
> The type uses the Spezi dependency injection of the `Bluetooth` module, the most common usage of the [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth) module. [You can learn more about the Spezi dependency injection mechanisms in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module-dependency).
The [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth)
module allows to declarative define your Bluetooth device using a [`BluetoothDevice`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetoothdevice) implementation and property wrappers
like [`Service`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/service) and [`Characteristic`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/characteristic).

```swift
import Spezi
import SpeziBluetooth
The below code examples demonstrate how you can implement your own Bluetooth device.

First of all we define our Bluetooth service by implementing a [`BluetoothService`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetoothservice).
We use the [`Characteristic`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/characteristic) property wrapper to declare its characteristics.
Note that the value types needs to be optional and conform to [`ByteEncodable`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/byteencodable), [`ByteDecodable`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bytedecoable) or [`ByteCodable`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bytecodable) respectively.

public class MyDeviceModule: DefaultInitializable, Module {
/// Spezi dependency injection of the `Bluetooth` module; see https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module-dependency for more details.
@Dependency private var bluetooth: Bluetooth
/// Injecting the `MyDevice` class in the SwiftUI environment as documented at https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/interactions-with-swiftui
@Model private var myDevice: MyDevice


public required init() {}


/// Configuration method to create the `MyDevice` and pass in the Bluetooth module.
public func configure() {
self.myDevice = MyDevice(bluetooth: bluetooth)
}
```swift
class DeviceInformationService: BluetoothService {
@Characteristic(id: "2A29")
var manufacturer: String?
@Characteristic(id: "2A26")
var firmwareRevision: String?
}
```

The next step is to define the Bluetooth services and caracteristics that you want to read from or get notified about:
We can use this Bluetooth service now in the `MyDevice` implementation as follows.

> Tip: We use the [`DeviceState`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/devicestate) and [`DeviceAction`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/deviceaction) property wrappers to get access to the device state and its actions. Those two
property wrappers can also be used within a [`BluetoothService`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetoothservice) type.

```swift
enum MyDeviceBluetoothConstants {
/// UUID for the example characteristic.
static let exampleCharacteristic = CBUUID(string: "a7779a75-f00a-05b4-147b-abf02f0d9b17")
/// Configuration for the example Bluetooth service.
static let exampleService = BluetoothService(
serviceUUID: CBUUID(string: "a7779a75-f00a-05b4-147b-abf02f0d9b17"),
characteristicUUIDs: [exampleCharacteristic]
)
class MyDevice: BluetoothDevice {
@DeviceState(\.id)
var id: UUID
@DeviceState(\.name)
var name: String?
@DeviceState(\.state)
var state: PeripheralState

@Service(id: "180A")
var deviceInformation = DeviceInformationService()

@DeviceAction(\.connect)
var connect
@DeviceAction(\.disconnect)
var disconnect

init() {} // required initializer
}
```

You will have to ensure that the [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth) module is correctly setup with the right services, e.g., as shown in the following example:
### Configure the Bluetooth Module

We use the above `BluetoothDevice` implementation to configure the [`Bluetooth`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth) module within the
[SpeziAppDelegate](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate).

```swift
class ExampleAppDelegate: SpeziAppDelegate {
import Spezi

class ExampleDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration {
Bluetooth(services: [MyDeviceBluetoothConstants.exampleService])
// ...
Bluetooth {
// Define which devices type to discover by what criteria .
// In this case we search for some custom FFF0 characteristic that is advertised.
Discover(MyDevice.self, by: .advertisedService("FFF0"))
}
}
}
}
```

The `MyDevice` type showcases the interaction with the ``BluetoothService`` and the implementation of the ``BluetoothMessageHandler`` protocol.
It does all the message handling, and is responsible for parsing the information.
### Using the Bluetooth Module

> [!NOTE]
> We highly recommend to use SwiftNIO [`ByteBuffer`](https://swiftpackageindex.com/apple/swift-nio/2.61.1/documentation/niocore/bytebuffer)s to parse more complex data coming in from the wire. You can learn more about creating a `ByteBuffer` from a Foundation `Data` instance using [NIOFoundationCompat](https://swiftpackageindex.com/apple/swift-nio/2.61.1/documentation/niofoundationcompat/niocore/bytebuffer).
Once you have the `Bluetooth` module configured within your Spezi app, you can access the module within your
[`Environment`](https://developer.apple.com/documentation/swiftui/environment).

You can use the [`scanNearbyDevices(enabled:with:autoConnect:)`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/swiftui/view/scanNearbyDevices(enabled:with:autoConnect:)) and [`autoConnect(enabled:with:)`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/swiftui/view/autoConnect(enabled:with:))
modifiers to scan for nearby devices and/or auto connect to the first available device. Otherwise, you can also manually start and stop scanning for nearby devices
using [`scanNearbyDevices(autoConnect:)`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth/scanNearbyDevices(autoConnect:)) and [`stopScanning()`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth/stopScanning()).

To retrieve the list of nearby devices you may use [`nearbyDevices(for:)`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/bluetooth/nearbyDevices(for:)).

> Tip: To easily access the first connected device, you can just query the SwiftUI Environment for your `BluetoothDevice` type.
Make sure to declare the property as optional using the respective [`Environment(_:)`](https://developer.apple.com/documentation/swiftui/environment/init(_:)-8slkf)
initializer.

The below code example demonstrates all these steps of retrieving the `Bluetooth` module from the environment, listing all nearby devices,
auto connecting to the first one and displaying some basic information of the currently connected device.

```swift
import Foundation
import Observation
import OSLog


@Observable
public class MyDevice: BluetoothMessageHandler {
/// Spezi dependency injection of the `Bluetooth` module; see https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module-dependency for more details.
private let bluetooth: Bluetooth
private let logger = Logger(subsystem: "edu.stanford.spezi.bluetooth", category: "Example")

/// Array of messages received from the Bluetooth connection.
private(set) public var messages: [String] = []


/// The current Bluetooth connection state.
public var bluetoothState: BluetoothState {
bluetooth.state
}


required init(bluetooth: Bluetooth) {
self.bluetooth = bluetooth
bluetooth.add(messageHandler: self)
}


/// Sends a string message over Bluetooth.
///
/// - Parameter information: The string message to be sent.
public func send(information: String) async throws {
try await bluetooth.write(
Data(information.utf8),
service: MyDeviceBluetoothConstants.exampleService.serviceUUID,
characteristic: MyDeviceBluetoothConstants.exampleCharacteristic
)
}

// Example implementation of the ``BluetoothMessageHandler`` requirements.
public func recieve(_ data: Data, service: CBUUID, characteristic: CBUUID) {
switch service {
case MyDeviceBluetoothConstants.exampleService.serviceUUID:
guard MyDeviceBluetoothConstants.exampleCharacteristic == characteristic else {
logger.debug("Unknown characteristic Id: \(MyDeviceBluetoothConstants.exampleCharacteristic)")
return
import SpeziBluetooth
import SwiftUI

struct MyView: View {
@Environment(Bluetooth.self)
var bluetooth
@Environment(MyDevice.self)
var myDevice: MyDevice?

var body: some View {
List {
if let myDevice {
Section {
Text("Device")
Spacer()
Text("\(myDevice.state.description)")
}
}

Section {
ForEach(bluetooth.nearbyDevices(for: MyDevice.self), id: \.id) { device in
Text("\(device.name ?? "unknown")")
}
} header: {
HStack {
Text("Devices")
.padding(.trailing, 10)
if bluetooth.isScanning {
ProgressView()
}
}
}

// Convert the received data into a string and append it to the messages array.
messages.append(String(decoding: data, as: UTF8.self))
default:
logger.debug("Unknown Service: \(service.uuidString)")
}
.scanNearbyDevices(with: bluetooth, autoConnect: true)
}
}
```
Expand Down
Loading