Skip to content

Commit

Permalink
Initial connection to the BioPot (#41)
Browse files Browse the repository at this point in the history
# 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
Supereg authored Nov 17, 2023
1 parent f992823 commit d47ccf3
Show file tree
Hide file tree
Showing 18 changed files with 889 additions and 58 deletions.
107 changes: 100 additions & 7 deletions NAMS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
"version" : "1.22.2"
"revision" : "9d108e9112aa1d65ce508facf804674546116d9c",
"version" : "1.22.3"
}
},
{
Expand Down Expand Up @@ -153,13 +153,22 @@
"version" : "0.7.3"
}
},
{
"identity" : "spezibluetooth",
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziBluetooth",
"state" : {
"revision" : "ae2d554434431ea5be5fc85a8dba8d55aa54c159",
"version" : "0.2.0"
}
},
{
"identity" : "spezicontact",
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziContact.git",
"state" : {
"revision" : "bd38bd769a20ebe89cec75ebc80dcbccfbd25230",
"version" : "0.5.0"
"revision" : "07515418259c68d6f5058a45b9e70ad0a0706680",
"version" : "0.5.1"
}
},
{
Expand Down Expand Up @@ -225,6 +234,15 @@
"version" : "0.6.2"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
Expand All @@ -234,13 +252,22 @@
"version" : "1.0.5"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c",
"version" : "2.62.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "7dade5ea5ab0d05a2dd28f9cc6a6ea0d50588857",
"version" : "1.25.0"
"revision" : "07f7f26ded8df9645c072f220378879c4642e063",
"version" : "1.25.1"
}
},
{
Expand Down
121 changes: 121 additions & 0 deletions NAMS/BioPot/Biopot.swift
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
110 changes: 110 additions & 0 deletions NAMS/BioPot/BiopotDevice.swift
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()
}
}
22 changes: 22 additions & 0 deletions NAMS/BioPot/Model/ByteCodable.swift
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
Loading

0 comments on commit d47ccf3

Please sign in to comment.