generated from StanfordSpezi/SpeziTemplateApplication
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial connection to the BioPot (#41)
# Initial connection to the BioPot ## ♻️ Current situation & Problem This PR adds some very basic and initial connection mechanism to NAMS to connect to the BioPot based on SpeziBluetooth 🚀 ## ⚙️ Release Notes * Added initial support for BioPot based on SpeziBluetooth. * Added some first characteristic models for encoding and decoding. ## 📚 Documentation Minimal documentation was added where necessary. A lot of context is tracked within our Notion space. ## ✅ Testing We added a initial setup for unit testing as well as a basic UI test! ### Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md).
- Loading branch information
Showing
18 changed files
with
889 additions
and
58 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import SpeziBluetooth | ||
import SpeziViews | ||
import SwiftUI | ||
|
||
|
||
struct Biopot: View { | ||
@Environment(BiopotDevice.self) | ||
private var biopot | ||
|
||
@State private var viewState: ViewState = .idle | ||
|
||
var body: some View { | ||
List { | ||
ListRow("Device") { | ||
Text(biopot.bluetoothState.localizedStringResource) | ||
} | ||
testingSupport | ||
|
||
|
||
if let info = biopot.deviceInfo { | ||
Section("Status") { | ||
ListRow("BATTERY") { | ||
BatteryIcon(percentage: Int(info.batteryLevel)) | ||
} | ||
ListRow("Charging") { | ||
if info.batteryCharging { | ||
Text("Yes") | ||
} else { | ||
Text("No") | ||
} | ||
} | ||
ListRow("Temperature") { | ||
Text("\(info.temperatureValue) °C") | ||
} | ||
} | ||
|
||
Section("Actions") { // section of testing actions | ||
AsyncButton("Read Device Configuration", state: $viewState) { | ||
try biopot.readBiopot(characteristic: BiopotDevice.Characteristic.biopotDeviceConfiguration) | ||
} | ||
} | ||
} else if biopot.bluetoothState == .scanning { | ||
Section { | ||
ProgressView() | ||
.listRowBackground(Color.clear) | ||
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) | ||
.frame(maxWidth: .infinity) | ||
} | ||
} | ||
} | ||
.viewStateAlert(state: $viewState) | ||
.navigationTitle("BioPot 3") | ||
.onChange(of: biopot.bluetoothState) { | ||
if biopot.bluetoothState != .connected { | ||
biopot.deviceInfo = nil | ||
} | ||
} | ||
} | ||
|
||
|
||
@MainActor @ViewBuilder private var testingSupport: some View { | ||
if FeatureFlags.testBiopot { | ||
Button("Receive Device Info") { | ||
biopot.deviceInfo = DeviceInformation( | ||
syncRatio: 0, | ||
syncMode: false, | ||
memoryWriteNumber: 0, | ||
memoryEraseMode: false, | ||
batteryLevel: 80, | ||
temperatureValue: 23, | ||
batteryCharging: false | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
extension BluetoothState: CustomLocalizedStringResourceConvertible { | ||
public var localizedStringResource: LocalizedStringResource { | ||
switch self { | ||
case .connected: | ||
return "Connected" | ||
case .disconnected: | ||
return "Disconnected" | ||
case .scanning: | ||
return "Scanning" | ||
case .poweredOff: | ||
return "Bluetooth Off" | ||
case .unauthorized: | ||
return "Bluetooth Unauthorized" | ||
} | ||
} | ||
} | ||
|
||
|
||
#if DEBUG | ||
import Spezi | ||
|
||
#Preview { | ||
class PreviewDelegate: SpeziAppDelegate { | ||
override var configuration: Configuration { | ||
Configuration { | ||
Bluetooth() | ||
BiopotDevice() | ||
} | ||
} | ||
} | ||
|
||
return Biopot() | ||
.spezi(PreviewDelegate()) | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import Combine | ||
import Foundation | ||
import NIOCore | ||
import OSLog | ||
import Spezi | ||
import SpeziBluetooth | ||
|
||
|
||
/// Model for the BioPot 3 device. | ||
/// | ||
/// If you need more information about bluetooth, you might find these resources helpful: | ||
/// * https://en.wikipedia.org/wiki/Bluetooth_Low_Energy#Software_model | ||
/// * https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth | ||
/// * https://devzone.nordicsemi.com/guides/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial | ||
@Observable | ||
class BiopotDevice: Module, EnvironmentAccessible, BluetoothMessageHandler, DefaultInitializable { | ||
private let logger = Logger(subsystem: "edu.stanford.nams", category: "BiopotDevice") | ||
|
||
@ObservationIgnored @Dependency private var bluetooth: Bluetooth | ||
|
||
var bluetoothState: BluetoothState { | ||
bluetooth.state | ||
} | ||
|
||
@MainActor var deviceInfo: DeviceInformation? | ||
|
||
|
||
required init() {} | ||
|
||
|
||
func configure() { | ||
bluetooth.add(messageHandler: self) | ||
} | ||
|
||
func recieve(_ data: Data, service: CBUUID, characteristic: CBUUID) async { | ||
guard service == Service.biopot else { | ||
logger.warning("Received data for unknown service: \(service)") | ||
return | ||
} | ||
|
||
logger.warning("Received data for biopot on service \(service.uuidString) for characteristic \(characteristic.uuidString): \(data.hexString())") | ||
|
||
var buffer = ByteBuffer(data: data) | ||
|
||
if characteristic == Characteristic.biopotDeviceInfo { | ||
guard let information = DeviceInformation(from: &buffer) else { | ||
return | ||
} | ||
|
||
await MainActor.run { | ||
self.deviceInfo = information | ||
} | ||
} else if characteristic == Characteristic.biopotDeviceConfiguration { | ||
guard let configuration = DeviceConfiguration(from: &buffer) else { | ||
return | ||
} | ||
|
||
logger.debug("Received configuration data: \("\(configuration)")") | ||
} else { | ||
logger.warning("Data on \(characteristic.uuidString)@\(service.uuidString) was unexpected and not processed!") | ||
} | ||
} | ||
|
||
func readBiopot(characteristic: CBUUID) throws { | ||
try bluetooth.read(service: Service.biopot, characteristic: characteristic) | ||
} | ||
} | ||
|
||
|
||
extension BiopotDevice { | ||
enum Service { | ||
static let biopot = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB") | ||
} | ||
} | ||
|
||
|
||
extension BiopotDevice { | ||
/// Characteristic definitions with access properties. | ||
/// | ||
/// Access properties: R: read, W: write, N: notify | ||
enum Characteristic { // naming is currently guess work | ||
/// Characteristic 1, as per the manual. RWN. | ||
static let biopotDeviceConfiguration = CBUUID(string: "0000FFF1-0000-1000-8000-00805F9B34FB") | ||
/// Characteristic 2, as per the manual. RW. | ||
static let biopotDataControl = CBUUID(string: "0000FFF2-0000-1000-8000-00805F9B34FB") | ||
/// Characteristic 3, as per the manual. RW. | ||
static let biopotDataAcquisition = CBUUID(string: "0000FFF3-0000-1000-8000-00805F9B34FB") | ||
/// Characteristic 4, as per the manual. RN. | ||
static let biopotDataStream = CBUUID(string: "0000FFF4-0000-1000-8000-00805F9B34FB") | ||
/// Characteristic 5, as per the manual. RW. | ||
static let biopotSamplingConfiguration = CBUUID(string: "0000FFF5-0000-1000-8000-00805F9B34FB") | ||
/// Characteristic 6, as per the manual. RN. | ||
static let biopotDeviceInfo = CBUUID(string: "0000FFF6-0000-1000-8000-00805F9B34FB") | ||
} | ||
} | ||
|
||
|
||
extension Data { | ||
func hexString() -> String { | ||
map { String(format: "%02hhx", $0) }.joined() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import NIOCore | ||
|
||
|
||
protocol ByteDecodable { | ||
init?(from byteBuffer: inout ByteBuffer) | ||
} | ||
|
||
|
||
protocol ByteEncodable { | ||
func encode(to byteBuffer: inout ByteBuffer) | ||
} | ||
|
||
|
||
typealias ByteCodable = ByteEncodable & ByteDecodable |
Oops, something went wrong.