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

SpeziBluetooth #106

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import edu.stanford.bdh.engagehf.bluetooth.spezi.configuration.DeviceDiscoveryDescriptor
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.BluetoothManager
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.BluetoothState
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice
import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID
import kotlinx.coroutines.flow.Flow
import kotlin.reflect.KClass
import kotlin.time.Duration

class Bluetooth(
@ApplicationContext val context: Context,
val configuration: Set<DeviceDiscoveryDescriptor>,
) {

private class Storage {
val nearbyDevices = mutableListOf<BluetoothDevice>()
}

private val manager = BluetoothManager(context)
private val storage = Storage()

val state: BluetoothState
get() = manager.state

val isScanning: Boolean
get() = manager.isScanning

val stateSubscription: Flow<BluetoothState>
get() = manager.stateSubscription

val hasConnectedDevices: Boolean
get() = manager.hasConnectedDevices

fun powerOn() {
manager.powerOn()
}

fun powerOff() {
manager.powerOff()
}

fun <Device : BluetoothDevice> nearbyDevices(type: KClass<Device>): List<Device> {
return storage.nearbyDevices.mapNotNull { it as? Device } // TODO
}

suspend fun <Device : BluetoothDevice> retrieveDevice(
uuid: BTUUID,
type: KClass<Device>,
): Device? {
TODO()
}

fun scanNearbyDevices(
minimumRssi: Int? = null,
advertisementStaleInterval: Duration? = null,
autoConnect: Boolean = false,
) {
TODO()
/*
manager.scanNearbyDevices(
discovery = configuration,
minimumRssi = minimumRssi, //
advertisementStaleInterval = advertisementStaleInterval,
autoConnect = autoConnect
)
*/
}

fun stopScanning() {
manager.stopScanning()
}
}

class BluetoothError(message: String, cause: Throwable? = null) : Exception(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi

import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothService
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties.Characteristic
import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID

data class DeviceInformationService(override val id: BTUUID = BTUUID("180A")) : BluetoothService {
val manufacturerName by Characteristic<String>("2A29")
val modelNumber by Characteristic<String>("2A24")
val serialNumber by Characteristic<String>("2A25")
val hardwareRevision by Characteristic<String>("2A27")
val firmwareRevision by Characteristic<String>("2A26")
val softwareRevision by Characteristic<String>("2A28")

val systemId by Characteristic<ULong>("2A23")
val regulatoryCertificationDataList by Characteristic<ByteArray>("2A2A")
val pnpId by Characteristic<PnPID>("2A50")
}

data class PnPID(val string: String) // TODO: Figure out what this is
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi

import edu.stanford.bdh.engagehf.bluetooth.spezi.core.BluetoothPeripheral
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties.DeviceAction
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties.DeviceState
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties.Service

class MockDevice : BluetoothDevice {
val id by DeviceState(BluetoothPeripheral::id)
val name by DeviceState(BluetoothPeripheral::name)
val state by DeviceState(BluetoothPeripheral::state)
val rssi by DeviceState(BluetoothPeripheral::rssi)
val nearby by DeviceState(BluetoothPeripheral::nearby)
val lastActivity by DeviceState(BluetoothPeripheral::lastActivity)

val connect by DeviceAction.connect

val deviceInformation by Service(DeviceInformationService())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi.configuration

import edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration.DiscoveryCriteria
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice
import kotlin.reflect.KClass

data class DeviceDiscoveryDescriptor(
val discoveryCriteria: DiscoveryCriteria,
val deviceType: KClass<BluetoothDevice>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi.configuration

import edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration.DiscoveryCriteria
import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice
import kotlin.reflect.KClass

data class Discover<Device : BluetoothDevice>(
val discoveryCriteria: DiscoveryCriteria,
val deviceType: KClass<Device>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi.core

import android.annotation.SuppressLint
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import androidx.annotation.RequiresPermission
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration.DeviceDescription
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration.DiscoveryDescription
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.BluetoothManagerStorage
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.BluetoothState
import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID
import kotlin.time.Duration

class BluetoothManager(context: Context) {
private val manager: BluetoothManager = context.getSystemService(BluetoothManager::class.java)

private val storage = BluetoothManagerStorage()
private val callbacks = mutableListOf<BluetoothManagerScanCallback>()

val isScanning: Boolean
get() = TODO()

val nearbyPeripherals: List<BluetoothPeripheral>
get() = TODO()

val state: BluetoothState
get() = storage.state

fun powerOn(): Unit = TODO()
fun powerOff(): Unit = TODO()

@RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
fun scanNearbyDevices(
discovery: Set<DiscoveryDescription>,
minimumRssi: Int?,
advertisementStaleInterval: Duration? = null,
autoConnect: Boolean = false,
) {
val callbacks = discovery.map {
BluetoothManagerScanCallback(
it,
minimumRssi,
advertisementStaleInterval,
autoConnect
)
}

this.callbacks.addAll(callbacks)

for (callback in callbacks) {
manager.adapter.bluetoothLeScanner.startScan(
callback.scanFilters,
callback.settings,
callback
)
}
}

@RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
fun stopScanning() {
for (callback in callbacks) {
manager.adapter.bluetoothLeScanner.stopScan(callback)
}
}

@SuppressLint("MissingPermission")
@RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
suspend fun retrievePeripheral(uuid: BTUUID, description: DeviceDescription): BluetoothPeripheral? {
for (callback in callbacks) {
val result = callback.results.firstOrNull {
it.device.uuids.contains(uuid.parcelUuid)
}
if (result != null) return BluetoothPeripheral(manager, result)
}
return null
}
}

internal class BluetoothManagerScanCallback(
val description: DiscoveryDescription,
val minimumRssi: Int?,
val advertisementStaleInterval: Duration?,
val autoConnect: Boolean,
) : ScanCallback() {
val scanFilters by lazy<List<ScanFilter>> {
val scanFilters = mutableListOf<ScanFilter>()
TODO()
}
val settings by lazy<ScanSettings> {
ScanSettings.Builder().build()
TODO()
}
val results = mutableListOf<ScanResult>()

@SuppressLint("MissingPermission")
@RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
override fun onScanResult(callbackType: Int, result: ScanResult?) {
if (result == null) return

when (callbackType) {
ScanSettings.CALLBACK_TYPE_MATCH_LOST -> {
println("onScanResult: Lost result")
results.removeIf {
it.device.uuids.contentEquals(
result.device.uuids ?: emptyArray()
)
}
}
ScanSettings.CALLBACK_TYPE_FIRST_MATCH,
ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
-> {
println("onScanResult: Received result")
results.add(result)
}
else ->
println("onScanResult: Unexpected callbackType $callbackType")
}
}

override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)

when (errorCode) {
SCAN_FAILED_INTERNAL_ERROR ->
println("onScanFailed: Internal Error")
SCAN_FAILED_SCANNING_TOO_FREQUENTLY ->
println("onScanFailed: Scanning too frequently")
SCAN_FAILED_ALREADY_STARTED ->
println("onScanFailed: Already started")
SCAN_FAILED_APPLICATION_REGISTRATION_FAILED ->
println("onScanFailed: Application registration failed")
SCAN_FAILED_FEATURE_UNSUPPORTED ->
println("onScanFailed: Feature unsupported")
SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES ->
println("onScanFailed: Out of hardware resources")
else ->
println("onScanFailed: Unknown error $errorCode")
}
}

override fun onBatchScanResults(results: MutableList<ScanResult>?) {
println("onBatchScanResults: Received ${results?.count() ?: 0} results")
results?.let { results.addAll(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi.core

import android.annotation.SuppressLint
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanResult
import android.content.Context
import android.os.ParcelUuid
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.AdvertisementData
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.GATTCharacteristic
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.GATTService
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.OnChangeRegistration
import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.PeripheralState
import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID
import java.util.Date

class BluetoothPeripheral(
private val manager: BluetoothManager,
private val result: ScanResult,
) {
class Storage

private var gatt: BluetoothGatt? = null
private val storage = Storage()

val id: BTUUID
@SuppressLint("MissingPermission")
get() = BTUUID(result.device.uuids.first())

val name: String?
@SuppressLint("MissingPermission")
get() = result.device.name

val rssi: Int get() = result.rssi
val state: PeripheralState get() = TODO()
val advertisementData: AdvertisementData
get() = AdvertisementData(
localName = result.scanRecord?.deviceName,
manufacturerData = null, // TODO: result.scanRecord?.manufacturerSpecificData,
serviceData = result.scanRecord?.serviceData?.mapKeys { BTUUID(it.key) },
serviceIdentifiers = result.scanRecord?.serviceUuids?.map { BTUUID(it) },
overflowServiceIdentifiers = null, // TODO: Figure out whether this exists or how it is set on iOS
txPowerLevel = result.scanRecord?.txPowerLevel,
isConnectable = result.isConnectable,
solicitedServiceIdentifiers = result.scanRecord?.serviceSolicitationUuids?.map {
BTUUID(
it
)
}
)

val services: List<GATTService>? get() = TODO()
val lastActivity: Date get() = TODO()
val nearby: Boolean get() = TODO()

@SuppressLint("MissingPermission")
suspend fun connect(context: Context) {
gatt = result.device.connectGatt(context, true, null)
}

fun disconnect(): Unit = TODO()

fun getService(id: BTUUID): GATTService? = TODO()
fun getCharacteristic(characteristicId: BTUUID, serviceId: ParcelUuid): GATTCharacteristic? =
TODO()

fun registerOnChangeHandler(
characteristic: GATTCharacteristic,
onChange: (ByteArray) -> Unit,
): OnChangeRegistration = TODO()

fun registerOnChangeHandler(
service: ParcelUuid,
characteristic: ParcelUuid,
onChange: (ByteArray) -> Unit,
): OnChangeRegistration = TODO()

fun enableNotifications(
enabled: Boolean = true,
serviceId: ParcelUuid,
characteristicId: ParcelUuid,
): Unit = TODO()

suspend fun setNotifications(enabled: Boolean, characteristic: GATTCharacteristic): Unit =
TODO()

suspend fun write(data: ByteArray, characteristic: GATTCharacteristic): Unit = TODO()

suspend fun writeWithoutResponse(data: ByteArray, characteristic: GATTCharacteristic): Unit =
TODO()

suspend fun read(characteristic: GATTCharacteristic): ByteArray = TODO()

suspend fun readRSSI(): Int = TODO()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration

import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID

data class CharacteristicDescription(
val identifier: BTUUID,
val discoverDescriptors: Boolean = false,
val autoRead: Boolean = true, // TODO: Think about renaming
)
Loading
Loading