diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/Bluetooth.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/Bluetooth.kt new file mode 100644 index 000000000..083b812f4 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/Bluetooth.kt @@ -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, +) { + + private class Storage { + val nearbyDevices = mutableListOf() + } + + private val manager = BluetoothManager(context) + private val storage = Storage() + + val state: BluetoothState + get() = manager.state + + val isScanning: Boolean + get() = manager.isScanning + + val stateSubscription: Flow + get() = manager.stateSubscription + + val hasConnectedDevices: Boolean + get() = manager.hasConnectedDevices + + fun powerOn() { + manager.powerOn() + } + + fun powerOff() { + manager.powerOff() + } + + fun nearbyDevices(type: KClass): List { + return storage.nearbyDevices.mapNotNull { it as? Device } // TODO + } + + suspend fun retrieveDevice( + uuid: BTUUID, + type: KClass, + ): 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) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/DeviceInformationService.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/DeviceInformationService.kt new file mode 100644 index 000000000..8ea5f234b --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/DeviceInformationService.kt @@ -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("2A29") + val modelNumber by Characteristic("2A24") + val serialNumber by Characteristic("2A25") + val hardwareRevision by Characteristic("2A27") + val firmwareRevision by Characteristic("2A26") + val softwareRevision by Characteristic("2A28") + + val systemId by Characteristic("2A23") + val regulatoryCertificationDataList by Characteristic("2A2A") + val pnpId by Characteristic("2A50") +} + +data class PnPID(val string: String) // TODO: Figure out what this is diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/MockDevice.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/MockDevice.kt new file mode 100644 index 000000000..a210932da --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/MockDevice.kt @@ -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()) +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/configuration/DeviceDiscoveryDescriptor.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/configuration/DeviceDiscoveryDescriptor.kt new file mode 100644 index 000000000..128d274f2 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/configuration/DeviceDiscoveryDescriptor.kt @@ -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, +) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/configuration/Discover.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/configuration/Discover.kt new file mode 100644 index 000000000..bec6b3edd --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/configuration/Discover.kt @@ -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( + val discoveryCriteria: DiscoveryCriteria, + val deviceType: KClass, +) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/BluetoothManager.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/BluetoothManager.kt new file mode 100644 index 000000000..d958ff54c --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/BluetoothManager.kt @@ -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() + + val isScanning: Boolean + get() = TODO() + + val nearbyPeripherals: List + 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, + 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> { + val scanFilters = mutableListOf() + TODO() + } + val settings by lazy { + ScanSettings.Builder().build() + TODO() + } + val results = mutableListOf() + + @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?) { + println("onBatchScanResults: Received ${results?.count() ?: 0} results") + results?.let { results.addAll(it) } + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/BluetoothPeripheral.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/BluetoothPeripheral.kt new file mode 100644 index 000000000..bd5bbedf1 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/BluetoothPeripheral.kt @@ -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? 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() +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/CharacteristicDescription.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/CharacteristicDescription.kt new file mode 100644 index 000000000..732c14593 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/CharacteristicDescription.kt @@ -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 +) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DeviceDescription.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DeviceDescription.kt new file mode 100644 index 000000000..e38974c47 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DeviceDescription.kt @@ -0,0 +1,10 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration + +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID + +data class DeviceDescription( + val services: Set? = null, +) { + fun description(identifier: BTUUID): ServiceDescription? = + services?.firstOrNull { it.identifier == identifier } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DiscoveryCriteria.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DiscoveryCriteria.kt new file mode 100644 index 000000000..0857ecaae --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DiscoveryCriteria.kt @@ -0,0 +1,21 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration + +import edu.stanford.bdh.engagehf.bluetooth.spezi.core.model.ManufacturerIdentifier +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID + +sealed class DiscoveryCriteria { + data class AdvertisedServices( + val uuids: List, + ) : DiscoveryCriteria() + + data class Accessory( + val manufacturer: ManufacturerIdentifier, + val uuids: List, + ) : DiscoveryCriteria() + + val discoveryIds: List get() = + when (this) { + is AdvertisedServices -> this.uuids + is Accessory -> this.uuids + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DiscoveryDescription.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DiscoveryDescription.kt new file mode 100644 index 000000000..9531d1bfa --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/DiscoveryDescription.kt @@ -0,0 +1,6 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration + +data class DiscoveryDescription( + val criteria: DiscoveryCriteria, + val device: DeviceDescription, +) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/ServiceDescription.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/ServiceDescription.kt new file mode 100644 index 000000000..25807545a --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/configuration/ServiceDescription.kt @@ -0,0 +1,11 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.configuration + +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID + +data class ServiceDescription( + val identifier: BTUUID, + val characteristics: Set? = null, +) { + fun description(identifier: BTUUID): CharacteristicDescription? = + characteristics?.firstOrNull { it.identifier == identifier } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/AdvertisementData.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/AdvertisementData.kt new file mode 100644 index 000000000..33685f393 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/AdvertisementData.kt @@ -0,0 +1,47 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID + +data class AdvertisementData( + val localName: String?, + val manufacturerData: ByteArray?, + val serviceData: Map?, + val serviceIdentifiers: List?, + val overflowServiceIdentifiers: List?, + val txPowerLevel: Int?, + val isConnectable: Boolean?, + val solicitedServiceIdentifiers: List?, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AdvertisementData + + if (localName != other.localName) return false + if (manufacturerData != null) { + if (other.manufacturerData == null) return false + if (!manufacturerData.contentEquals(other.manufacturerData)) return false + } else if (other.manufacturerData != null) return false + if (serviceData != other.serviceData) return false + if (serviceIdentifiers != other.serviceIdentifiers) return false + if (overflowServiceIdentifiers != other.overflowServiceIdentifiers) return false + if (txPowerLevel != other.txPowerLevel) return false + if (isConnectable != other.isConnectable) return false + if (solicitedServiceIdentifiers != other.solicitedServiceIdentifiers) return false + + return true + } + + override fun hashCode(): Int { + var result = localName?.hashCode() ?: 0 + result = 31 * result + (manufacturerData?.contentHashCode() ?: 0) + result = 31 * result + (serviceData?.hashCode() ?: 0) + result = 31 * result + (serviceIdentifiers?.hashCode() ?: 0) + result = 31 * result + (overflowServiceIdentifiers?.hashCode() ?: 0) + result = 31 * result + (txPowerLevel?.hashCode() ?: 0) + result = 31 * result + (isConnectable?.hashCode() ?: 0) + result = 31 * result + (solicitedServiceIdentifiers?.hashCode() ?: 0) + return result + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothError.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothError.kt new file mode 100644 index 000000000..b9175b889 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothError.kt @@ -0,0 +1,25 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +import android.os.ParcelUuid + +sealed class BluetoothError : Error() { + @Suppress("UnusedPrivateMember") + data object IncompatibleDataFormat : BluetoothError() { + private fun readResolve(): Any = IncompatibleDataFormat // TODO: What is this? + } + + data class NotPresent( + val service: ParcelUuid?, + val characteristic: ParcelUuid, + ) : BluetoothError() + + data class ControlPointRequiresNotifying( + val service: ParcelUuid, + val characteristic: ParcelUuid, + ) : BluetoothError() + + data class ControlPointInProgress( + val service: ParcelUuid, + val characteristic: ParcelUuid, + ) : BluetoothError() +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothManagerStorage.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothManagerStorage.kt new file mode 100644 index 000000000..f0e69fb96 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothManagerStorage.kt @@ -0,0 +1,21 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +import edu.stanford.bdh.engagehf.bluetooth.spezi.core.BluetoothPeripheral +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID + +// TODO: What's up with this observable thing there? +class BluetoothManagerStorage { + val connectedDevices = mutableSetOf() + val retrievedPeripherals = mutableMapOf() + var isScanning: Boolean = false + + val hasConnectedDevices: Boolean + get() = connectedDevices.isNotEmpty() + + var state: BluetoothState = BluetoothState.UNKNOWN + private set + + fun update(state: BluetoothState) { + this.state = state + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothState.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothState.kt new file mode 100644 index 000000000..a77ceed14 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/BluetoothState.kt @@ -0,0 +1,9 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +enum class BluetoothState(private val rawValue: UByte) { + UNKNOWN(0u), + POWERED_OFF(1u), + UNSUPPORTED(2u), + UNAUTHORIZED(3u), + POWERED_ON(4u), +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/GATTCharacteristic.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/GATTCharacteristic.kt new file mode 100644 index 000000000..c154d437d --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/GATTCharacteristic.kt @@ -0,0 +1,3 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +class GATTCharacteristic diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/GATTService.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/GATTService.kt new file mode 100644 index 000000000..5997908cd --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/GATTService.kt @@ -0,0 +1,3 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +class GATTService diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/ManufacturerIdentifier.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/ManufacturerIdentifier.kt new file mode 100644 index 000000000..7884925bc --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/ManufacturerIdentifier.kt @@ -0,0 +1,3 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +data class ManufacturerIdentifier(val rawValue: UShort) diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/OnChangeRegistration.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/OnChangeRegistration.kt new file mode 100644 index 000000000..3ee33a2a5 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/OnChangeRegistration.kt @@ -0,0 +1,5 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +class OnChangeRegistration { + fun cancel(): Unit = TODO() +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/PeripheralState.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/PeripheralState.kt new file mode 100644 index 000000000..e9d9ce100 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/PeripheralState.kt @@ -0,0 +1,9 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +// TODO: We use Device pretty consistently, why now Peripheral? +enum class PeripheralState(private val value: UByte) { + DISCONNECTED(0u), + CONNECTING(1u), + CONNECTED(2u), + DISCONNECTING(3u), +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/WriteType.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/WriteType.kt new file mode 100644 index 000000000..fd5c693c0 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/core/model/WriteType.kt @@ -0,0 +1,6 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.core.model + +enum class WriteType { + WITH_RESPONSE, + WITHOUT_RESPONSE, +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/environment/CompositionLocal.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/environment/CompositionLocal.kt new file mode 100644 index 000000000..8ae45b280 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/environment/CompositionLocal.kt @@ -0,0 +1,22 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.environment + +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.ConnectedDevices +import kotlin.reflect.KClass + +private var localConnectedDevices = mutableMapOf, Any>() + +fun localConnectedDevices(type: KClass): ProvidableCompositionLocal> { + @Suppress("UNCHECKED_CAST") // TODO: Think about whether we can get rid of this. + val existingValue = localConnectedDevices[type]?.let { return@let it as? ProvidableCompositionLocal> } + if (existingValue != null) return existingValue + val newValue = compositionLocalOf { ConnectedDevices() } + localConnectedDevices[type] = newValue + return newValue +} + +val localAdvertisementStaleInterval = compositionLocalOf { null as Double? } + +val localMinimumRSSI = compositionLocalOf { null as Int? } diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/BluetoothDevice.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/BluetoothDevice.kt new file mode 100644 index 000000000..d14ee06b5 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/BluetoothDevice.kt @@ -0,0 +1,5 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model + +interface BluetoothDevice { // TODO: Think about making this an abstract class + // TODO: iOS forces an empty constructor here, is there a way to do so in Kotlin? +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/BluetoothService.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/BluetoothService.kt new file mode 100644 index 000000000..bfe4e878a --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/BluetoothService.kt @@ -0,0 +1,7 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model + +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID + +interface BluetoothService { + val id: BTUUID +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/BluetoothConnectAction.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/BluetoothConnectAction.kt new file mode 100644 index 000000000..ae19bc832 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/BluetoothConnectAction.kt @@ -0,0 +1,11 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.actions + +import edu.stanford.bdh.engagehf.bluetooth.spezi.core.BluetoothPeripheral +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties.BluetoothPeripheralAction + +// TODO: Implement and check if we can really not get rid of injecting closures +data class BluetoothConnectAction(val peripheral: BluetoothPeripheral) : BluetoothPeripheralAction { + suspend fun invoke() { + peripheral.connect() + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/BluetoothDisconnectAction.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/BluetoothDisconnectAction.kt new file mode 100644 index 000000000..88bf3a8a6 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/BluetoothDisconnectAction.kt @@ -0,0 +1,11 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.actions + +import edu.stanford.bdh.engagehf.bluetooth.spezi.core.BluetoothPeripheral +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties.BluetoothPeripheralAction + +data class BluetoothDisconnectAction(val peripheral: BluetoothPeripheral) : + BluetoothPeripheralAction { + suspend fun invoke() { + peripheral.disconnect() + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/ReadRSSIAction.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/ReadRSSIAction.kt new file mode 100644 index 000000000..5ff30d07e --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/actions/ReadRSSIAction.kt @@ -0,0 +1,8 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.actions + +import edu.stanford.bdh.engagehf.bluetooth.spezi.core.BluetoothPeripheral +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties.BluetoothPeripheralAction + +data class ReadRSSIAction(val peripheral: BluetoothPeripheral) : BluetoothPeripheralAction { + suspend fun invoke(): Int = TODO() +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/BluetoothPeripheralAction.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/BluetoothPeripheralAction.kt new file mode 100644 index 000000000..a6158172a --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/BluetoothPeripheralAction.kt @@ -0,0 +1,3 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties + +interface BluetoothPeripheralAction diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/Characteristic.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/Characteristic.kt new file mode 100644 index 000000000..6d4b08ea1 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/Characteristic.kt @@ -0,0 +1,30 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties + +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothService +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.propertySupport.CharacteristicAccessor +import edu.stanford.bdh.engagehf.bluetooth.spezi.utils.BTUUID +import kotlin.reflect.KProperty + +// TODO: We might need to build this different, except for the public interface +class Characteristic( + val identifier: BTUUID, + val notify: Boolean = false, + val autoRead: Boolean = true, +) { + operator fun getValue( + thisRef: Service, + property: KProperty<*>, + ): Value? = TODO() + + val accessor: CharacteristicAccessor get() = TODO() + + companion object { + operator fun invoke( + identifier: String, + notify: Boolean = false, + autoRead: Boolean = true, + ): Characteristic { + return Characteristic(BTUUID(identifier), notify, autoRead) + } + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/DeviceAction.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/DeviceAction.kt new file mode 100644 index 000000000..f10583c96 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/DeviceAction.kt @@ -0,0 +1,34 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties + +import edu.stanford.bdh.engagehf.bluetooth.spezi.BluetoothError +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.actions.BluetoothConnectAction +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.actions.BluetoothDisconnectAction +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.actions.ReadRSSIAction +import kotlin.reflect.KProperty + +data class DeviceAction(val createAction: (BluetoothPeripheral) -> Action) { + class Storage(var peripheral: BluetoothPeripheral? = null) + + private val storage: Storage = Storage() + + operator fun getValue( + thisRef: Device, + property: KProperty<*>, + ): Action { + return storage.peripheral?.let { + createAction(it) + } ?: throw BluetoothError("Not found") + } + + fun inject(peripheral: BluetoothPeripheral?) { + storage.peripheral = peripheral + } + + companion object { + val connect get() = DeviceAction { BluetoothConnectAction(it) } + val disconnect get() = DeviceAction { BluetoothDisconnectAction(it) } + val readRSSI get() = DeviceAction { ReadRSSIAction(it) } + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/DeviceState.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/DeviceState.kt new file mode 100644 index 000000000..5f4fc487a --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/DeviceState.kt @@ -0,0 +1,22 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties + +import edu.stanford.bdh.engagehf.bluetooth.spezi.Bluetooth +import edu.stanford.bdh.engagehf.bluetooth.spezi.BluetoothError +import edu.stanford.bdh.engagehf.bluetooth.spezi.core.BluetoothPeripheral +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 + +data class DeviceState(val property: KProperty1) { + private var peripheral: BluetoothPeripheral? = null + + fun inject(bluetooth: Bluetooth, peripheral: BluetoothPeripheral) { + this.peripheral = peripheral + } + + operator fun getValue(thisRef: BluetoothDevice, property: KProperty<*>): Value { + return this.peripheral?.let { + this.property.get(it) + } ?: throw BluetoothError("Not found") + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/Service.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/Service.kt new file mode 100644 index 000000000..67730216b --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/properties/Service.kt @@ -0,0 +1,11 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.properties + +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothService +import kotlin.reflect.KProperty + +data class Service(val service: S) { + operator fun getValue(thisRef: BluetoothDevice, property: KProperty<*>): S { + return service + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/propertySupport/CharacteristicAccessor.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/propertySupport/CharacteristicAccessor.kt new file mode 100644 index 000000000..45f205966 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/model/propertySupport/CharacteristicAccessor.kt @@ -0,0 +1,3 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.model.propertySupport + +class CharacteristicAccessor diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/utils/BTUUID.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/utils/BTUUID.kt new file mode 100644 index 000000000..388f23dca --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/utils/BTUUID.kt @@ -0,0 +1,18 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.utils + +import android.os.ParcelUuid + +data class BTUUID(val parcelUuid: ParcelUuid) { + companion object { + private const val SHORT_UUID_LENGTH = 4 + + operator fun invoke(string: String): BTUUID { + val uuid = if (string.length == SHORT_UUID_LENGTH) { + "0000$string-0000-1000-8000-00805F9B34FB" + } else { + string + } + return BTUUID(ParcelUuid.fromString(uuid)) + } + } +} diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/utils/ConnectedDevices.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/utils/ConnectedDevices.kt new file mode 100644 index 000000000..6ad6242a7 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/spezi/utils/ConnectedDevices.kt @@ -0,0 +1,5 @@ +package edu.stanford.bdh.engagehf.bluetooth.spezi.utils + +import edu.stanford.bdh.engagehf.bluetooth.spezi.model.BluetoothDevice + +data class ConnectedDevices(val devices: List = emptyList()) diff --git a/internal/detekt-config.yml b/internal/detekt-config.yml index f5dd2b75b..8599301af 100644 --- a/internal/detekt-config.yml +++ b/internal/detekt-config.yml @@ -475,7 +475,7 @@ naming: maximumVariableNameLength: 64 VariableMinLength: active: true - minimumVariableNameLength: 3 + minimumVariableNameLength: 2 VariableNaming: active: true variablePattern: '[a-z][A-Za-z0-9]*'