mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-01-07 00:34:33 +01:00
Migration to new BLEK library (#143)
* Changed view. * Clear messages. * Clear messages. * Add or delete configuration. * Fixed configuration picker. * Edit configuration. * Create new macro. * removed unnecessary resource files. * Fixed running macro command. * Delete macro * Edit macro * Changed to peripheral name. * Show peripheral name. * Fixed Eol tab design. * Removed icon resource * String changes * Removed any permission from home view. * Clear device after disconnection. * 1 line app bar * Changed missing services text. * Throughput service view changes. * Throughput service fixes. * Removed unused resources. * Fixed Health temperature profile. * Show heart rate. * Fixed hrs view. * Show heart rate data from left to right in the chart. * Changed chart color, solid, and scroll to see history. * Horizontal grid hidden, in case needed. * HTS view update * Changed padding. * Removed circular icon background. * Updated Battery level view. * Updated hrs body sensor location. * Moved ui mappers into view. * Updated gls view. * Changed focus color. * Fixed issue with job. * Fixed bps. * Added Blood pressure feature uuid. * Added blood pressure feature data. * Added rscs feature data. * Fixed cscs view. * Show supported features. * Fixed ui * Suspend the service discovery for GLS and CGMS until bonding is completed. * Added suspend on the function level. * Bonding state check only to cgms service * Removed stacktrace print. * Make cgms record available within a scrollable box * Changed to gray color. * removed padding * Fix height for output section. * onExpand click event. * Added todo for 9th item. * Removed unused code block. * When in focus, reduce the hint text alpha value. * Show empty text error. * Clear focus on tap outside. * Add border when focused. * Propagate focus changes. * CGM graph * Added sample of one to many uart configuration database. * Added device and configuration entities. * Fixed issue with only showing last item from the list. * Changed configuration database irrespective of device address. * File rename. * Added last configuration datastore. * Check if configuration name is unique * Removed Macro text. * Included x and y axis data points. * Added channel sounding service uuid. * Upgraded agp version to 2.7. * Added channel sounding manager. * Downgraded datastore preference to 1.1.4. * Changed to nordic colors. * Added ranging permission. * channel sounding repository * channel sounding service data * channel sounding profile * channel sounding profile in viewmodel * channel sounding manager class * channel sounding testing * CS service characteristics * Create bonding before channel sounding connection. * Clean up. * Added LBS profile * Read/write data to LBS * LBS ui events * LBS service * LBS profile * LBS ui * Agp upgrade * Fixed LBS profile * Removed focus * Changed macro size to 9 * Changed macro color * Show macro in bottom sheet * View refactoring * Added Blek dependency * Added utils dependency * rename * Removed unused event * reorganization * uart macro view update * background color update * different color for input and output message type * Changed to uart event * removed duplicate * rename * auto scroll to new record * removed unused dependency * Fixed crash with ChannelSoundingManager injection. * Require bonding only if it has bonding information * Changed disconnection * CGMS graph * changes in the home view * Home view fixes * changed color * Show MacroEol character in the input message. * Home view icon fixes. * Cadence data parser fixes * Fixed CSC settings view. * Fixed rscs view * hiding graphs until its finished * Removed duplicate * Fixed RSCS view * Fixed notification icon * fixed csc module name * Fixed icon cutoff * Fixed CSCDataParser * Fixed CGMS profile * Fixed GLS view * Fixed GLS strings * Fixed HTS view * Fixed HTS view * title change * Added hts timestamp * Deleted verbose text * UART: changed macro/configuration to preset * UART: fixed input text field * UART: removed expandable/collapsable preset * UART: added extra warning to delete action * UART: don't trim message end. * UART: message section * UART: configuration fixed * UART: configuration fixed * Fix crash when disconnecting before MTU change completes * Disabled incomplete PRX profile * Moved non-composable lambdas to parameters * refactoring display text * Fixed channel sounding screen * Disconnect on missing services before navigation * Fixed label name * Tailored disconnection message. * Tailored disconnection message. * Moved profile file to utils * App analytics events and modes * Integrated analytics with the profile actions. * Show only first non-battery service if multiple services are present. * Fixed window insets for camera notch. * Fixed glucose measurement context. * Fixed glucose concentration unit. * Fixed duplicate analytics update. * rename * refactoring text * Handled disconnecting event. * Replaced with LazyColumn * Fixed window insets * Replaced TitleAppBar with NordicAppBar * Show device address * Show multiple service names if available. * Fixed padding * BPS: Fixed waiting for measurement view. * BPS: view * GLS: Fixed padding * Ui: Fixed dialog * RSCS: fixed distance formatting error * CGMS: ui consistency * DFS: ui fixes * Replaced local scanner with common library scanner. * Fixed padding * reorganization * Removed previous uart module * Text with animated three dots * HTS: text fixes * formatting texts * changed text style * fixed string * Fixed HRS, not completed * DFS: fixed ui * HRS: graph fixes * UART: scroll up when keyboard is visible * Uart input: Add focus * Uart fix: input text field * UART: created rememberImeState * HRS: heart rate ui fixes * profile view scrollable fix * DFS: ui fixes * Fixed logger * Check if the battery characteristics supports NOTIFY or INDICATE property * Dependency update * Changed background color * cleanup * Fixed distance measurement data update. * Filtered devices with testing address * Added preview data * Fixed section view * Fixed elevation view * Removed duplicate views * Fixes control points * String fixes * Elevation view fixes * Range slider view update * Fixed DFS views * Fixed DFS ui * Fixed DFS views * Separated views * Separated profile viewmodel into individual profile view models. * AGP upgrade * Job canceled and make jobs null on clear * Profile name update * Request maximum MTU size only if it is not already set. * Fixed null pointer exception * Battery characteristics read property check * Fixed early mtu request * Removed garbage states * Removed logs * Removed multiple vertical scroll * Fixed padding * Ui fixes * File reorganization * Fixed previous configuration not loading on reconnection * Removed unused files * Dependency update * Renamed module name * Removed unused dependencies * Added param * Removed unused code block * Code optimization * Removed unused file * Readme update * Hide Channel sounding until implementation is complete * Handled initial state closed * revert changes * Added library as module placeholder * Fixed multiple flows for the same peripheral * Request mtu size only when needed * Readme update
This commit is contained in:
2
profile_manager/src/main/AndroidManifest.xml
Normal file
2
profile_manager/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
@@ -0,0 +1,64 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureFeatureParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureMeasurementParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.bps.IntermediateCuffPressureParser
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.BPSRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
|
||||
private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb")
|
||||
private val BPF_CHARACTERISTIC_UUID = UUID.fromString("00002A49-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class BPSManager : ServiceManager {
|
||||
override val profile: Profile = Profile.BPS
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
remoteService.characteristics.firstOrNull { it.uuid == BPM_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { BloodPressureMeasurementParser.parse(it) }
|
||||
?.onEach { BPSRepository.updateBPSData(deviceId, it) }
|
||||
?.onCompletion { BPSRepository.clear(deviceId) }
|
||||
?.catch { e ->
|
||||
e.printStackTrace()
|
||||
Timber.e(e)
|
||||
}?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics.firstOrNull { it.uuid == ICP_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { IntermediateCuffPressureParser.parse(it) }
|
||||
?.onEach { BPSRepository.updateICPData(deviceId, it) }
|
||||
?.onCompletion { BPSRepository.clear(deviceId) }
|
||||
?.catch { e ->
|
||||
e.printStackTrace()
|
||||
Timber.e(e)
|
||||
}?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics.firstOrNull { it.uuid == BPF_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.read()
|
||||
?.let {
|
||||
BloodPressureFeatureParser.parse(it)
|
||||
}?.also { featureData ->
|
||||
BPSRepository.updateBPSFeatureData(deviceId, featureData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.toolbox.profile.parser.battery.BatteryLevelParser
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.BatteryRepository
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val BATTERY_LEVEL_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class BatteryManager : ServiceManager {
|
||||
override val profile: Profile = Profile.BATTERY
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val batteryChar = remoteService.characteristics
|
||||
.firstOrNull { it.uuid == BATTERY_LEVEL_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
|
||||
batteryChar?.let { characteristic ->
|
||||
// If the characteristic supports READ, read the initial value
|
||||
if (characteristic.properties.contains(CharacteristicProperty.READ)) {
|
||||
try {
|
||||
characteristic.read()
|
||||
.let {
|
||||
BatteryLevelParser.parse(it)
|
||||
}
|
||||
?.let { batteryLevel ->
|
||||
BatteryRepository.updateBatteryLevel(deviceId, batteryLevel)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Error reading battery level: ${e.message}")
|
||||
}
|
||||
}
|
||||
// Check if the characteristic supports NOTIFY or INDICATE property
|
||||
if (characteristic.properties.contains(CharacteristicProperty.NOTIFY)
|
||||
|| characteristic.properties.contains(CharacteristicProperty.INDICATE)
|
||||
) {
|
||||
// Start subscription for battery level updates
|
||||
characteristic.subscribe()
|
||||
.mapNotNull { BatteryLevelParser.parse(it) }
|
||||
.onEach { batteryLevel ->
|
||||
BatteryRepository.updateBatteryLevel(deviceId, batteryLevel)
|
||||
}
|
||||
.onCompletion {
|
||||
BatteryRepository.clear(deviceId)
|
||||
}
|
||||
.catch { e ->
|
||||
Timber.e(e)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMFeatureParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMMeasurementParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMSpecificOpsControlPointParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMStatusParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMErrorCode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMOpCode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.CGMSpecificOpsControlPointDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointInputParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.NumberOfRecordsData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordAccessControlPointData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.ResponseData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.racp.RACPOpCode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.racp.RACPResponseCode
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.CGMRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.android.toolbox.lib.utils.logAndReport
|
||||
import no.nordicsemi.android.toolbox.lib.utils.tryOrLog
|
||||
import no.nordicsemi.android.toolbox.profile.data.CGMRecordWithSequenceNumber
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb")
|
||||
private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb")
|
||||
private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb")
|
||||
private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class CGMManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.CGM
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
// 1. Subscribe to CGM Measurement first
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == CGM_MEASUREMENT_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { CGMMeasurementParser.parse(it) }?.onEach { cgmRecords ->
|
||||
if (sessionStartTime == 0L && !recordAccessRequestInProgress) {
|
||||
val timeOffset = cgmRecords.minOf { it.timeOffset }
|
||||
sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L
|
||||
}
|
||||
|
||||
cgmRecords.map {
|
||||
val timestamp = sessionStartTime + it.timeOffset * 60000L
|
||||
CGMRecordWithSequenceNumber(it.timeOffset, it, timestamp)
|
||||
}.apply {
|
||||
CGMRepository.onMeasurementDataReceived(deviceId, this)
|
||||
}
|
||||
}?.onCompletion { CGMRepository.clear(deviceId) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.launchIn(scope)
|
||||
|
||||
// 2. Subscribe to RACP and store reference
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == RACP_UUID.toKotlinUuid() }
|
||||
?.let { racpCharacteristic ->
|
||||
recordAccessControlPointCharacteristic = racpCharacteristic
|
||||
racpCharacteristic.subscribe()
|
||||
.mapNotNull { RecordAccessControlPointParser.parse(it) }
|
||||
.onEach { onAccessControlPointDataReceived(deviceId, it, scope) }
|
||||
.catch { it.logAndReport() }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
// 3. Read CGM Feature
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == CGM_FEATURE_UUID.toKotlinUuid() }
|
||||
?.takeIf { it.properties.contains(CharacteristicProperty.READ) }
|
||||
?.read()
|
||||
?.let { CGMFeatureParser.parse(it) }
|
||||
?.let { secured = it.features.e2eCrcSupported }
|
||||
|
||||
// 4. Read CGM Status
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == CGM_STATUS_UUID.toKotlinUuid() }
|
||||
?.takeIf { it.properties.contains(CharacteristicProperty.READ) }
|
||||
?.read()
|
||||
?.let { CGMStatusParser.parse(it) }
|
||||
?.let {
|
||||
if (!it.status.sessionStopped) {
|
||||
sessionStartTime = System.currentTimeMillis() - it.timeOffset * 60000L
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Subscribe to Ops Control Point
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == CGM_OPS_CONTROL_POINT_UUID.toKotlinUuid() }
|
||||
?.let { cgmOpsControlPointCharacteristic ->
|
||||
opsControlPointCharacteristic = cgmOpsControlPointCharacteristic
|
||||
cgmOpsControlPointCharacteristic.subscribe()
|
||||
.mapNotNull { CGMSpecificOpsControlPointParser.parse(it) }
|
||||
.onEach {
|
||||
if (it.isOperationCompleted) {
|
||||
sessionStartTime =
|
||||
if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION)
|
||||
System.currentTimeMillis() else 0
|
||||
} else if (
|
||||
it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION &&
|
||||
it.errorCode == CGMErrorCode.CGM_ERROR_PROCEDURE_NOT_COMPLETED
|
||||
) {
|
||||
sessionStartTime = 0
|
||||
} else if (it.requestCode == CGMOpCode.CGM_OP_CODE_STOP_SESSION) {
|
||||
sessionStartTime = 0
|
||||
}
|
||||
}
|
||||
.onCompletion { CGMRepository.clear(deviceId) }
|
||||
.catch { it.logAndReport() }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
// 6. Write to Ops Control if needed
|
||||
if (sessionStartTime == 0L) {
|
||||
try {
|
||||
opsControlPointCharacteristic.write(
|
||||
CGMSpecificOpsControlPointDataParser.startSession(secured),
|
||||
WriteType.WITH_RESPONSE
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Error while starting session: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccessControlPointDataReceived(
|
||||
deviceId: String,
|
||||
data: RecordAccessControlPointData,
|
||||
scope: CoroutineScope
|
||||
) = scope.launch {
|
||||
when (data) {
|
||||
is NumberOfRecordsData -> onNumberOfRecordsReceived(deviceId, data.numberOfRecords)
|
||||
|
||||
is ResponseData -> when (data.responseCode) {
|
||||
RACPResponseCode.RACP_RESPONSE_SUCCESS ->
|
||||
onRecordAccessOperationCompleted(deviceId, data.requestCode)
|
||||
|
||||
RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND ->
|
||||
onRecordAccessOperationCompletedWithNoRecordsFound(deviceId)
|
||||
|
||||
else -> onRecordAccessOperationError(deviceId, data.responseCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationError(deviceId: String, responseCode: RACPResponseCode) {
|
||||
CGMRepository.updateNewRequestStatus(
|
||||
deviceId = deviceId,
|
||||
requestStatus = when (responseCode) {
|
||||
RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED -> RequestStatus.NOT_SUPPORTED
|
||||
else -> RequestStatus.FAILED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompletedWithNoRecordsFound(deviceId: String) {
|
||||
CGMRepository.updateNewRequestStatus(
|
||||
deviceId = deviceId,
|
||||
requestStatus = RequestStatus.SUCCESS
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompleted(deviceId: String, requestCode: RACPOpCode) {
|
||||
CGMRepository.updateNewRequestStatus(
|
||||
deviceId = deviceId,
|
||||
requestStatus = when (requestCode) {
|
||||
RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||
else -> RequestStatus.SUCCESS
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun onNumberOfRecordsReceived(
|
||||
deviceId: String,
|
||||
numberOfRecords: Int,
|
||||
) {
|
||||
val state = CGMRepository.getData(deviceId)
|
||||
val highestSequenceNumber = state.value
|
||||
.records
|
||||
.maxByOrNull { it.sequenceNumber }
|
||||
?.sequenceNumber
|
||||
?: -1
|
||||
|
||||
if (numberOfRecords > 0)
|
||||
tryOrLog {
|
||||
recordAccessControlPointCharacteristic
|
||||
.write(
|
||||
if (state.value.records.isNotEmpty()) {
|
||||
RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(
|
||||
highestSequenceNumber.toShort()
|
||||
)
|
||||
} else {
|
||||
RecordAccessControlPointInputParser.reportAllStoredRecords()
|
||||
},
|
||||
WriteType.WITH_RESPONSE
|
||||
)
|
||||
}
|
||||
CGMRepository.updateNewRequestStatus(
|
||||
deviceId = deviceId,
|
||||
requestStatus = RequestStatus.SUCCESS
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var recordAccessControlPointCharacteristic: RemoteCharacteristic
|
||||
private lateinit var opsControlPointCharacteristic: RemoteCharacteristic
|
||||
|
||||
private var recordAccessRequestInProgress = false
|
||||
private var sessionStartTime: Long = 0
|
||||
private var secured = false
|
||||
|
||||
suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) {
|
||||
writeOrSetStatusFailed(deviceId) {
|
||||
recordAccessControlPointCharacteristic.write(
|
||||
when (workingMode) {
|
||||
WorkingMode.ALL -> RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords()
|
||||
WorkingMode.LAST -> RecordAccessControlPointInputParser.reportLastStoredRecord()
|
||||
WorkingMode.FIRST -> RecordAccessControlPointInputParser.reportFirstStoredRecord()
|
||||
},
|
||||
WriteType.WITH_RESPONSE
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private suspend fun writeOrSetStatusFailed(
|
||||
deviceId: String,
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
try {
|
||||
block()
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
CGMRepository.updateNewRequestStatus(deviceId, RequestStatus.FAILED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.toolbox.profile.parser.csc.CSCDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.CSCRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class CSCManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.CSC
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == CSC_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull {
|
||||
CSCDataParser.parse(it, CSCRepository.getData(deviceId).value.data.wheelSize)
|
||||
}
|
||||
?.onEach { CSCRepository.onCSCDataChanged(deviceId, it) }
|
||||
?.catch { it.printStackTrace() }
|
||||
?.onCompletion { CSCRepository.clear(deviceId) }
|
||||
?.launchIn(scope)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val RAS_FEATURES = UUID.fromString("00002C14-0000-1000-8000-00805F9B34FB")
|
||||
private val REALTIME_RANGING_DATA = UUID.fromString("00002C15-0000-1000-8000-00805F9B34FB")
|
||||
private val RAS_ON_DEMAND_RD = UUID.fromString("00002C16-0000-1000-8000-00805F9B34FB")
|
||||
private val RAS_CP = UUID.fromString("00002C17-0000-1000-8000-00805F9B34FB")
|
||||
private val RAS_RD_READY = UUID.fromString("00002C18-0000-1000-8000-00805F9B34FB")
|
||||
private val RAS_RD_OVERWRITTEN = UUID.fromString("00002C19-0000-1000-8000-00805F9B34FB")
|
||||
|
||||
internal class ChannelSoundingManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.CHANNEL_SOUNDING
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class, ExperimentalStdlibApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
remoteService.characteristics.firstOrNull {
|
||||
it.uuid == RAS_FEATURES.toKotlinUuid()
|
||||
}
|
||||
?.read()
|
||||
?.let {
|
||||
val rasFeature = RasFeatureParser.parse(it)
|
||||
Timber.tag("ChannelSoundingManager").d("Ranging Feature: $rasFeature")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class RasFeature(
|
||||
val realTimeRangingData: Boolean,
|
||||
val retrieveLostSegments: Boolean,
|
||||
val abortOperation: Boolean,
|
||||
val filterRangingData: Boolean,
|
||||
)
|
||||
|
||||
|
||||
object RasFeatureParser {
|
||||
|
||||
fun parse(data: ByteArray): RasFeature {
|
||||
require(data.size >= 4) { "RAS Features characteristic must be at least 4 bytes." }
|
||||
|
||||
val featureBits = (data[0].toInt() and 0xFF) or
|
||||
((data[1].toInt() and 0xFF) shl 8) or
|
||||
((data[2].toInt() and 0xFF) shl 16) or
|
||||
((data[3].toInt() and 0xFF) shl 24)
|
||||
|
||||
return RasFeature(
|
||||
realTimeRangingData = featureBits and (1 shl 0) != 0,
|
||||
retrieveLostSegments = featureBits and (1 shl 1) != 0,
|
||||
abortOperation = featureBits and (1 shl 2) != 0,
|
||||
filterRangingData = featureBits and (1 shl 3) != 0
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthalMeasurementDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.ddf.DDFDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMeasurementDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.DFSRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.android.toolbox.lib.utils.logAndReport
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val DISTANCE_MEASUREMENT_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("21490001-494a-4573-98af-f126af76f490")
|
||||
private val AZIMUTH_MEASUREMENT_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("21490002-494a-4573-98af-f126af76f490")
|
||||
private val ELEVATION_MEASUREMENT_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("21490003-494a-4573-98af-f126af76f490")
|
||||
private val DDF_FEATURE_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("21490004-494a-4573-98af-f126af76f490")
|
||||
private val CONTROL_POINT_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("21490005-494a-4573-98af-f126af76f490")
|
||||
|
||||
internal class DFSManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.DFS
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == AZIMUTH_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { AzimuthalMeasurementDataParser().parse(it) }
|
||||
?.onEach { DFSRepository.addNewAzimuth(deviceId, it) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.onCompletion { DFSRepository.clear(deviceId) }
|
||||
?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == DISTANCE_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { DistanceMeasurementDataParser().parse(it) }
|
||||
?.onEach { DFSRepository.addNewDistance(deviceId, it) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.onCompletion { DFSRepository.clear(deviceId) }
|
||||
?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == ELEVATION_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { ElevationMeasurementDataParser().parse(it) }
|
||||
?.onEach { DFSRepository.addNewElevation(deviceId, it) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.onCompletion { DFSRepository.clear(deviceId) }
|
||||
?.launchIn(scope)
|
||||
|
||||
val ddfFeatureCharacteristics = remoteService.characteristics
|
||||
.firstOrNull { it.uuid == DDF_FEATURE_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.apply { ddfFeatureCharacteristic = this }
|
||||
val isReadPropertyAvailable = ddfFeatureCharacteristics
|
||||
?.properties?.contains(CharacteristicProperty.READ)
|
||||
if (isReadPropertyAvailable == true) {
|
||||
ddfFeatureCharacteristics.read()
|
||||
.let { DDFDataParser().parse(it) }
|
||||
?.apply { DFSRepository.setAvailableDistanceModes(deviceId, this) }
|
||||
} else {
|
||||
Timber.e("Characteristic Property READ is not available for $ddfFeatureCharacteristics")
|
||||
}
|
||||
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == CONTROL_POINT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.apply { controlPointCharacteristic = this }
|
||||
?.subscribe()
|
||||
?.mapNotNull { ControlPointDataParser().parse(it) }
|
||||
?.onEach { DFSRepository.onControlPointDataReceived(deviceId, it, scope) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.onCompletion { DFSRepository.clear(deviceId) }
|
||||
?.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var controlPointCharacteristic: RemoteCharacteristic
|
||||
private lateinit var ddfFeatureCharacteristic: RemoteCharacteristic
|
||||
|
||||
private val MCPD_ENABLED_BYTES = byteArrayOf(0x01, 0x01)
|
||||
private val RTT_ENABLED_BYTES = byteArrayOf(0x01, 0x00)
|
||||
private val CHECK_CONFIG_BYTES = byteArrayOf(0x0A)
|
||||
|
||||
suspend fun enableDistanceMode(deviceId: String, mode: DistanceMode) {
|
||||
val data = when (mode) {
|
||||
DistanceMode.MCPD -> MCPD_ENABLED_BYTES
|
||||
DistanceMode.RTT -> RTT_ENABLED_BYTES
|
||||
}
|
||||
try {
|
||||
controlPointCharacteristic.write(data, WriteType.WITH_RESPONSE)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to enable distance mode: $mode for device: $deviceId")
|
||||
DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.FAILED)
|
||||
} finally {
|
||||
DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.SUCCESS)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun checkForCurrentDistanceMode(deviceId: String) {
|
||||
try {
|
||||
controlPointCharacteristic.write(
|
||||
CHECK_CONFIG_BYTES,
|
||||
writeType = WriteType.WITH_RESPONSE
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to check current distance mode for device: $deviceId")
|
||||
DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.FAILED)
|
||||
} finally {
|
||||
DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.SUCCESS)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkAvailableFeatures(deviceId: String) {
|
||||
DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.PENDING)
|
||||
val isReadPropertyAvailable = ddfFeatureCharacteristic
|
||||
.properties.contains(CharacteristicProperty.READ)
|
||||
if (isReadPropertyAvailable) {
|
||||
ddfFeatureCharacteristic.read()
|
||||
.let { DDFDataParser().parse(it) }
|
||||
?.apply {
|
||||
DFSRepository.setAvailableDistanceModes(deviceId, this)
|
||||
}
|
||||
} else {
|
||||
Timber.e("Characteristic Property READ is not available for $ddfFeatureCharacteristic")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.GlucoseMeasurementContextParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.GlucoseMeasurementParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointInputParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.NumberOfRecordsData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordAccessControlPointData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.ResponseData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.racp.RACPOpCode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.racp.RACPResponseCode
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.GLSRepository
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.GLSRepository.updateNewRequestStatus
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.android.toolbox.lib.utils.logAndReport
|
||||
import no.nordicsemi.android.toolbox.lib.utils.tryOrLog
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val GLUCOSE_MEASUREMENT_CHARACTERISTIC =
|
||||
UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
|
||||
private val GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC =
|
||||
UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb")
|
||||
private val GLUCOSE_FEATURE_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb")
|
||||
private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class GLSManager : ServiceManager {
|
||||
override val profile: Profile = Profile.GLS
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == GLUCOSE_MEASUREMENT_CHARACTERISTIC.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { GlucoseMeasurementParser.parse(it) }
|
||||
?.onEach { GLSRepository.updateNewRecord(deviceId, it) }
|
||||
?.onCompletion { GLSRepository.clear(deviceId) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { GlucoseMeasurementContextParser.parse(it) }
|
||||
?.onEach { GLSRepository.updateWithNewContext(deviceId, it) }
|
||||
?.onCompletion { GLSRepository.clear(deviceId) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == RACP_CHARACTERISTIC.toKotlinUuid() }
|
||||
?.apply { recordAccessControlPointCharacteristic = this }
|
||||
?.subscribe()
|
||||
?.mapNotNull { RecordAccessControlPointParser.parse(it) }
|
||||
?.onEach { onAccessControlPointDataReceived(deviceId, it, scope) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccessControlPointDataReceived(
|
||||
deviceId: String,
|
||||
data: RecordAccessControlPointData,
|
||||
scope: CoroutineScope
|
||||
) = scope.launch {
|
||||
when (data) {
|
||||
is NumberOfRecordsData -> onNumberOfRecordsReceived(deviceId, data.numberOfRecords)
|
||||
|
||||
is ResponseData -> when (data.responseCode) {
|
||||
RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted(
|
||||
deviceId,
|
||||
data.requestCode
|
||||
)
|
||||
|
||||
RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED ->
|
||||
onRecordAccessOperationCompletedWithNoRecordsFound(deviceId)
|
||||
|
||||
else -> onRecordAccessOperationError(deviceId, data.responseCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationError(deviceId: String, responseCode: RACPResponseCode) {
|
||||
updateNewRequestStatus(
|
||||
deviceId,
|
||||
when (responseCode) {
|
||||
RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED -> RequestStatus.NOT_SUPPORTED
|
||||
else -> RequestStatus.FAILED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompleted(deviceId: String, requestCode: RACPOpCode) {
|
||||
updateNewRequestStatus(
|
||||
deviceId,
|
||||
when (requestCode) {
|
||||
RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||
else -> RequestStatus.SUCCESS
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRecordAccessOperationCompletedWithNoRecordsFound(deviceId: String) {
|
||||
updateNewRequestStatus(deviceId, RequestStatus.SUCCESS)
|
||||
}
|
||||
|
||||
private suspend fun onNumberOfRecordsReceived(
|
||||
deviceId: String,
|
||||
numberOfRecords: Int,
|
||||
) {
|
||||
val state = GLSRepository.getData(deviceId)
|
||||
val highestSequenceNumber = state.value
|
||||
.records
|
||||
.keys
|
||||
.maxByOrNull { it.sequenceNumber }
|
||||
?.sequenceNumber ?: -1
|
||||
|
||||
if (numberOfRecords > 0)
|
||||
tryOrLog {
|
||||
recordAccessControlPointCharacteristic
|
||||
.write(
|
||||
if (state.value.records.isNotEmpty()) {
|
||||
RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(
|
||||
highestSequenceNumber.toShort()
|
||||
)
|
||||
} else {
|
||||
RecordAccessControlPointInputParser.reportAllStoredRecords()
|
||||
},
|
||||
WriteType.WITH_RESPONSE
|
||||
)
|
||||
}
|
||||
updateNewRequestStatus(deviceId, RequestStatus.SUCCESS)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var recordAccessControlPointCharacteristic: RemoteCharacteristic
|
||||
|
||||
suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) {
|
||||
writeOrSetStatusFailed(deviceId) {
|
||||
recordAccessControlPointCharacteristic.write(
|
||||
when (workingMode) {
|
||||
WorkingMode.ALL -> RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords()
|
||||
WorkingMode.LAST -> RecordAccessControlPointInputParser.reportLastStoredRecord()
|
||||
WorkingMode.FIRST -> RecordAccessControlPointInputParser.reportFirstStoredRecord()
|
||||
},
|
||||
WriteType.WITH_RESPONSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeOrSetStatusFailed(
|
||||
deviceId: String,
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
try {
|
||||
block()
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
updateNewRequestStatus(deviceId, RequestStatus.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.hrs.BodySensorLocationParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.hrs.HRSDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.HRSRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb")
|
||||
private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class HRSManager : ServiceManager {
|
||||
override val profile: Profile = Profile.HRS
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
remoteService.characteristics.firstOrNull { it.uuid == HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { HRSDataParser.parse(it) }
|
||||
?.onEach { data ->
|
||||
HRSRepository.updateHRSData(deviceId, data)
|
||||
}
|
||||
?.onCompletion { HRSRepository.clear(deviceId) }
|
||||
?.catch { e ->
|
||||
// Handle the error
|
||||
e.printStackTrace()
|
||||
Timber.e(e)
|
||||
}?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics.firstOrNull { it.uuid == BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.read()
|
||||
?.let { BodySensorLocationParser.parse(it) }
|
||||
?.let { bodySensorLocation ->
|
||||
HRSRepository.updateBodySensorLocation(deviceId, bodySensorLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.hts.HTSDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.HTSRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val HTS_MEASUREMENT_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class HTSManager : ServiceManager {
|
||||
override val profile: Profile = Profile.HTS
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
remoteService.characteristics.firstOrNull { it.uuid == HTS_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { HTSDataParser.parse(it) }
|
||||
?.onEach { htsData ->
|
||||
HTSRepository.updateHTSData(deviceId, htsData)
|
||||
}
|
||||
?.onCompletion { HTSRepository.clear(deviceId) }
|
||||
?.catch { e ->
|
||||
Timber.e(e)
|
||||
}?.launchIn(scope)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.LBSRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val BLINKY_BUTTON_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("00001524-1212-EFDE-1523-785FEABCD123")
|
||||
private val BLINKY_LED_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("00001525-1212-EFDE-1523-785FEABCD123")
|
||||
|
||||
internal class LBSManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.LBS
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
// Ensure the characteristic is initialized before writing
|
||||
ledWriteCharacteristics = remoteService.characteristics.firstOrNull {
|
||||
it.uuid == BLINKY_LED_CHARACTERISTIC_UUID.toKotlinUuid()
|
||||
} ?: throw IllegalStateException("LED characteristic not found")
|
||||
|
||||
val blinkyCharacteristics = remoteService.characteristics.firstOrNull {
|
||||
it.uuid == BLINKY_BUTTON_CHARACTERISTIC_UUID.toKotlinUuid()
|
||||
}
|
||||
|
||||
// Subscribe to the button state changes.
|
||||
blinkyCharacteristics?.subscribe()
|
||||
?.mapNotNull { ButtonStateParser.parse(it) }
|
||||
?.onEach { LBSRepository.updateButtonState(deviceId, it) }
|
||||
?.catch {
|
||||
Timber.e("Error observing button state: ${it.message}")
|
||||
}
|
||||
?.onCompletion {
|
||||
LBSRepository.clear(deviceId)
|
||||
}?.launchIn(scope)
|
||||
|
||||
// Read the initial state of the button
|
||||
try {
|
||||
blinkyCharacteristics?.read()
|
||||
?.let { ButtonStateParser.parse(it) }
|
||||
?.let { LBSRepository.updateButtonState(deviceId, it) }
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Error reading button state: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var ledWriteCharacteristics: RemoteCharacteristic
|
||||
|
||||
/**
|
||||
* Writes the LED state to the Blinky LED characteristic.
|
||||
*
|
||||
* @param deviceId The ID of the device to which the LED state should be written.
|
||||
* @param ledState The desired state of the LED (true for ON, false for OFF).
|
||||
*/
|
||||
suspend fun writeToBlinkyLED(
|
||||
deviceId: String,
|
||||
ledState: Boolean
|
||||
) {
|
||||
val data = byteArrayOf((0x01.takeIf { ledState } ?: 0x00).toByte())
|
||||
|
||||
try {
|
||||
if (::ledWriteCharacteristics.isInitialized) {
|
||||
ledWriteCharacteristics.write(data, WriteType.WITHOUT_RESPONSE)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Error writing to Blinky LED characteristic: ${e.message}")
|
||||
} finally {
|
||||
LBSRepository.updateLedState(deviceId, ledState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ButtonStateParser {
|
||||
fun parse(data: ByteArray): Boolean {
|
||||
return if (data.isNotEmpty()) {
|
||||
data[0].toInt() == 0x01
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSFeatureDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.RSCSRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.android.toolbox.lib.utils.logAndReport
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val RSC_MEASUREMENT_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("00002A53-0000-1000-8000-00805F9B34FB")
|
||||
private val RSC_FEATURE_CHARACTERISTIC_UUID =
|
||||
UUID.fromString("00002A54-0000-1000-8000-00805F9B34FB")
|
||||
|
||||
internal class RSCSManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.RSCS
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == RSC_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { RSCSDataParser.parse(it) }
|
||||
?.onEach { RSCSRepository.onRSCSDataChanged(deviceId, it) }
|
||||
?.catch { it.logAndReport() }
|
||||
?.onCompletion { RSCSRepository.clear(deviceId) }
|
||||
?.launchIn(scope)
|
||||
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == RSC_FEATURE_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.read()
|
||||
?.let {
|
||||
RSCSFeatureDataParser.parse(it)
|
||||
}?.also {
|
||||
RSCSRepository.updateRSCSFeatureData(deviceId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
|
||||
sealed interface ServiceManager {
|
||||
val profile: Profile
|
||||
suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.BATTERY_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.BPS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.CGMS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.CSC_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.DF_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.GLS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.HRS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.HTS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.LBS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.RSCS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.THROUGHPUT_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.UART_SERVICE_UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
object ServiceManagerFactory {
|
||||
|
||||
private val serviceManagers = mapOf(
|
||||
BATTERY_SERVICE_UUID to ::BatteryManager,
|
||||
BPS_SERVICE_UUID to ::BPSManager,
|
||||
CSC_SERVICE_UUID to ::CSCManager,
|
||||
CGMS_SERVICE_UUID to ::CGMManager,
|
||||
DF_SERVICE_UUID to ::DFSManager,
|
||||
GLS_SERVICE_UUID to ::GLSManager,
|
||||
HTS_SERVICE_UUID to ::HTSManager,
|
||||
HRS_SERVICE_UUID to ::HRSManager,
|
||||
RSCS_SERVICE_UUID to ::RSCSManager,
|
||||
THROUGHPUT_SERVICE_UUID to ::ThroughputManager,
|
||||
UART_SERVICE_UUID to ::UARTManager,
|
||||
// CHANNEL_SOUND_SERVICE_UUID to ::ChannelSoundingManager,
|
||||
LBS_SERVICE_UUID to ::LBSManager,
|
||||
// Add more service UUIDs to handler mappings as needed
|
||||
).mapKeys { it.key.toKotlinUuid() }
|
||||
|
||||
fun createServiceManager(serviceUuid: Uuid): ServiceManager? {
|
||||
return serviceManagers[serviceUuid]?.invoke()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import no.nordicsemi.android.toolbox.profile.parser.throughput.ThroughputDataParser
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.ThroughputRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.android.toolbox.profile.data.NumberOfBytes
|
||||
import no.nordicsemi.android.toolbox.profile.data.NumberOfSeconds
|
||||
import no.nordicsemi.android.toolbox.profile.data.ThroughputInputType
|
||||
import no.nordicsemi.android.toolbox.profile.data.WritingStatus
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import no.nordicsemi.kotlin.ble.core.util.chunked
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val THROUGHPUT_CHAR_UUID = UUID.fromString("00001524-0000-1000-8000-00805F9B34FB")
|
||||
|
||||
internal class ThroughputManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.THROUGHPUT
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
try {
|
||||
remoteService.characteristics
|
||||
.firstOrNull { it.uuid == THROUGHPUT_CHAR_UUID.toKotlinUuid() }
|
||||
?.also { writeCharacteristicProperty = it }
|
||||
} finally {
|
||||
ThroughputRepository.clearData(deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var writeCharacteristicProperty: RemoteCharacteristic
|
||||
|
||||
suspend fun writeRequest(
|
||||
deviceId: String,
|
||||
maxWriteValueLength: Int,
|
||||
inputType: ThroughputInputType,
|
||||
) {
|
||||
try {
|
||||
ThroughputRepository.updateWriteStatus(deviceId, WritingStatus.IN_PROGRESS)
|
||||
when (inputType) {
|
||||
is NumberOfBytes -> {
|
||||
writeBytesData(maxWriteValueLength, inputType.numberOfBytes)
|
||||
}
|
||||
|
||||
is NumberOfSeconds -> {
|
||||
writeTimesData(maxWriteValueLength, inputType.numberOfSeconds)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("ThroughputService").e("Error ${e.message}")
|
||||
} finally {
|
||||
readThroughputMetrics(deviceId)
|
||||
ThroughputRepository.updateWriteStatus(deviceId, WritingStatus.COMPLETED)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeBytesData(
|
||||
maxWriteValueLength: Int,
|
||||
numberOfBytes: Int
|
||||
) {
|
||||
val array = ByteArray(numberOfBytes) { 0x3D }
|
||||
writeCharacteristicProperty.write(
|
||||
data = byteArrayOf(0x3D),
|
||||
writeType = WriteType.WITHOUT_RESPONSE
|
||||
)
|
||||
array.chunked(maxWriteValueLength).map {
|
||||
writeCharacteristicProperty.write(
|
||||
data = it,
|
||||
writeType = WriteType.WITHOUT_RESPONSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeTimesData(
|
||||
maxWriteValueLength: Int,
|
||||
numberOfSecond: Int
|
||||
) {
|
||||
val array = ByteArray(maxWriteValueLength) { 0x3D }
|
||||
val startTime = System.currentTimeMillis()
|
||||
writeCharacteristicProperty.write(
|
||||
data = byteArrayOf(0x3D),
|
||||
writeType = WriteType.WITHOUT_RESPONSE
|
||||
)
|
||||
while (System.currentTimeMillis() - startTime < numberOfSecond * 1000) {
|
||||
writeCharacteristicProperty.write(
|
||||
data = array,
|
||||
writeType = WriteType.WITHOUT_RESPONSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readThroughputMetrics(deviceId: String) {
|
||||
try {
|
||||
// Read data after write operation is complete
|
||||
val readData = writeCharacteristicProperty.read()
|
||||
|
||||
// Parse the read data
|
||||
ThroughputDataParser.parse(data = readData)?.let {
|
||||
ThroughputRepository.updateThroughput(deviceId, it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("ThroughputService").e("Error ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.toolbox.profile.manager.repository.UartRepository
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import no.nordicsemi.kotlin.ble.core.util.chunked
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
private val UART_RX_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
|
||||
private val UART_TX_CHARACTERISTIC_UUID: UUID =
|
||||
UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
|
||||
|
||||
internal class UARTManager : ServiceManager {
|
||||
override val profile: Profile
|
||||
get() = Profile.UART
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
override suspend fun observeServiceInteractions(
|
||||
deviceId: String,
|
||||
remoteService: RemoteService,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
withContext(scope.coroutineContext) {
|
||||
remoteService.characteristics.firstOrNull { it.uuid == UART_TX_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.subscribe()
|
||||
?.mapNotNull { String(it) }
|
||||
?.onEach { UartRepository.onNewMessageReceived(deviceId, it) }
|
||||
?.catch { it.printStackTrace() }
|
||||
?.onCompletion {
|
||||
// Clear the device resources.
|
||||
UartRepository.clear(deviceId)
|
||||
}
|
||||
?.launchIn(scope)
|
||||
|
||||
val writeCharacteristics =
|
||||
remoteService.characteristics.firstOrNull { it.uuid == UART_RX_CHARACTERISTIC_UUID.toKotlinUuid() }
|
||||
?.also { rxCharacteristic = it }
|
||||
writeCharacteristics?.properties?.let {
|
||||
if (it.contains(CharacteristicProperty.WRITE_WITHOUT_RESPONSE)) {
|
||||
rxCharacteristicWriteType = WriteType.WITHOUT_RESPONSE
|
||||
} else if (it.contains(CharacteristicProperty.WRITE)) {
|
||||
rxCharacteristicWriteType = WriteType.WITH_RESPONSE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var rxCharacteristic: RemoteCharacteristic
|
||||
private var rxCharacteristicWriteType: WriteType? = null
|
||||
|
||||
suspend fun sendText(
|
||||
device: String,
|
||||
message: String,
|
||||
maxWriteLength: Int,
|
||||
macroEolUnicodeMessage: String? = null,
|
||||
) {
|
||||
val messageBytes = message.toByteArray()
|
||||
try {
|
||||
if (rxCharacteristicWriteType == null) {
|
||||
Timber.e("Write type not set.")
|
||||
// Todo: Handle this case.
|
||||
} else {
|
||||
messageBytes.chunked(maxWriteLength).forEach {
|
||||
rxCharacteristic.write(it, rxCharacteristicWriteType!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("UARTService").e("Error ${e.message}")
|
||||
} finally {
|
||||
UartRepository.onNewMessageSent(device, macroEolUnicodeMessage ?: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureFeatureData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureMeasurementData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.bps.IntermediateCuffPressureData
|
||||
import no.nordicsemi.android.toolbox.profile.data.BPSServiceData
|
||||
|
||||
object BPSRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<BPSServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): Flow<BPSServiceData> {
|
||||
return _dataMap.getOrPut(deviceId) { MutableStateFlow(BPSServiceData()) }
|
||||
}
|
||||
|
||||
fun updateBPSData(deviceId: String, bpsData: BloodPressureMeasurementData) {
|
||||
_dataMap[deviceId]?.update { it.copy(bloodPressureMeasurement = bpsData) }
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
fun updateICPData(deviceId: String, icpData: IntermediateCuffPressureData) {
|
||||
_dataMap[deviceId]?.update { it.copy(intermediateCuffPressure = icpData) }
|
||||
}
|
||||
|
||||
fun updateBPSFeatureData(deviceId: String, bpsFeatureData: BloodPressureFeatureData) {
|
||||
_dataMap[deviceId]?.update { it.copy(bloodPressureFeature = bpsFeatureData) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.data.BatteryServiceData
|
||||
|
||||
object BatteryRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<BatteryServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): Flow<BatteryServiceData> {
|
||||
return _dataMap.getOrPut(deviceId) { MutableStateFlow(BatteryServiceData()) }
|
||||
}
|
||||
|
||||
fun updateBatteryLevel(deviceId: String, data: Int) {
|
||||
_dataMap[deviceId]?.update { it.copy(batteryLevel = data) }
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.toolbox.profile.data.CGMRecordWithSequenceNumber
|
||||
import no.nordicsemi.android.toolbox.profile.data.CGMServiceData
|
||||
import no.nordicsemi.android.toolbox.profile.manager.CGMManager
|
||||
|
||||
object CGMRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<CGMServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): StateFlow<CGMServiceData> =
|
||||
_dataMap.getOrPut(deviceId) { MutableStateFlow(CGMServiceData()) }
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
fun onMeasurementDataReceived(deviceId: String, data: List<CGMRecordWithSequenceNumber>) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
records = it.records + data
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNewRequestStatus(deviceId: String, requestStatus: RequestStatus) {
|
||||
_dataMap[deviceId]?.update { it.copy(requestStatus = requestStatus) }
|
||||
}
|
||||
|
||||
private fun clearState(deviceId: String) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
records = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) {
|
||||
clearState(deviceId)
|
||||
updateNewRequestStatus(deviceId, RequestStatus.PENDING)
|
||||
_dataMap[deviceId]?.update { it.copy(workingMode = workingMode) }
|
||||
CGMManager.requestRecord(deviceId, workingMode)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.data.CSCServiceData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.csc.CSCData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.csc.SpeedUnit
|
||||
import no.nordicsemi.android.toolbox.profile.parser.csc.WheelSize
|
||||
|
||||
object CSCRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<CSCServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): StateFlow<CSCServiceData> = _dataMap.getOrPut(deviceId) {
|
||||
MutableStateFlow(CSCServiceData())
|
||||
}
|
||||
|
||||
fun onCSCDataChanged(deviceId: String, cscData: CSCData) {
|
||||
_dataMap[deviceId]?.update { it.copy(data = cscData) }
|
||||
}
|
||||
|
||||
fun setWheelSize(deviceId: String, wheelSize: WheelSize) {
|
||||
_dataMap[deviceId]?.update { currentValue ->
|
||||
currentValue.copy(
|
||||
data = CSCData(
|
||||
wheelSize = wheelSize
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSpeedUnit(deviceId: String, speedUnit: SpeedUnit) {
|
||||
_dataMap[deviceId]?.update { it.copy(speedUnit = speedUnit) }
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import no.nordicsemi.android.toolbox.profile.data.ChannelSoundingServiceData
|
||||
|
||||
object ChannelSoundingRepository {
|
||||
private val dataMap = mutableMapOf<String, MutableStateFlow<ChannelSoundingServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): MutableStateFlow<ChannelSoundingServiceData> =
|
||||
dataMap.getOrPut(deviceId) { MutableStateFlow(ChannelSoundingServiceData()) }
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthMeasurementData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointChangeModeError
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointChangeModeSuccess
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointCheckModeError
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointCheckModeSuccess
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointResult
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.ddf.DDFData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMeasurementData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.McpdMeasurementData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.RttMeasurementData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.directionFinder.toDistanceMode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.toolbox.profile.data.DFSServiceData
|
||||
import no.nordicsemi.android.toolbox.profile.data.SensorData
|
||||
import no.nordicsemi.android.toolbox.profile.data.SensorValue
|
||||
import no.nordicsemi.android.toolbox.profile.data.directionFinder.MeasurementSection
|
||||
import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range
|
||||
import no.nordicsemi.android.toolbox.profile.manager.DFSManager
|
||||
|
||||
object DFSRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<DFSServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): StateFlow<DFSServiceData> = _dataMap.getOrPut(deviceId) {
|
||||
MutableStateFlow(DFSServiceData())
|
||||
}
|
||||
|
||||
fun updateSelectedDevice(deviceId: String, device: PeripheralBluetoothAddress) {
|
||||
_dataMap[deviceId]?.update { it.copy(selectedDevice = device) }
|
||||
}
|
||||
|
||||
fun addNewAzimuth(deviceId: String, azimuth: AzimuthMeasurementData) {
|
||||
_dataMap[deviceId]?.update { current ->
|
||||
val validatedAzimuth = azimuth.copy(azimuth = azimuth.azimuth.coerceIn(0, 359))
|
||||
val key = validatedAzimuth.address
|
||||
val sensorData = current.data[key] ?: SensorData()
|
||||
val azimuths = sensorData.azimuth ?: SensorValue()
|
||||
val newAzimuths = azimuths.copyWithNewValue(validatedAzimuth)
|
||||
val newSensorData = sensorData.copy(azimuth = newAzimuths)
|
||||
val newDevicesData = current.data.toMutableMap().apply {
|
||||
put(key, newSensorData)
|
||||
}.toMap()
|
||||
current.copy(data = newDevicesData)
|
||||
}
|
||||
}
|
||||
|
||||
fun addNewDistance(deviceId: String, distance: DistanceMeasurementData) {
|
||||
when (distance) {
|
||||
is McpdMeasurementData -> addDistance(deviceId, distance, DistanceMode.MCPD)
|
||||
is RttMeasurementData -> addDistance(deviceId, distance, DistanceMode.RTT)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
private fun addDistance(
|
||||
deviceId: String,
|
||||
distance: DistanceMeasurementData,
|
||||
distanceMode: DistanceMode,
|
||||
) {
|
||||
_dataMap[deviceId]?.update { current ->
|
||||
val key = distance.address
|
||||
val sensorData = current.data[key] ?: SensorData()
|
||||
val newSensorData = when (distanceMode) {
|
||||
DistanceMode.MCPD -> sensorData.copy(
|
||||
mcpdDistance = sensorData.mcpdDistance
|
||||
?.copyWithNewValue(distance as McpdMeasurementData) ?: SensorValue(),
|
||||
distanceMode = distanceMode
|
||||
)
|
||||
|
||||
DistanceMode.RTT -> sensorData.copy(
|
||||
rttDistance = sensorData.rttDistance
|
||||
?.copyWithNewValue(distance as RttMeasurementData) ?: SensorValue(),
|
||||
distanceMode = distanceMode
|
||||
)
|
||||
}
|
||||
val newDevicesData = current.data.toMutableMap().apply {
|
||||
put(key, newSensorData)
|
||||
}.toMap()
|
||||
current.copy(data = newDevicesData)
|
||||
}
|
||||
}
|
||||
|
||||
fun addNewElevation(deviceId: String, elevation: ElevationMeasurementData) {
|
||||
_dataMap[deviceId]?.update { current ->
|
||||
val validatedElevation =
|
||||
elevation.copy(elevation = elevation.elevation.coerceIn(-90, 90))
|
||||
val key = validatedElevation.address
|
||||
val sensorData = current.data[key] ?: SensorData()
|
||||
val elevations = sensorData.elevation ?: SensorValue()
|
||||
val newElevation = elevations.copyWithNewValue(validatedElevation)
|
||||
val newSensorData = sensorData.copy(elevation = newElevation)
|
||||
val newDevicesData = current.data.toMutableMap().apply {
|
||||
put(key, newSensorData)
|
||||
}.toMap()
|
||||
current.copy(data = newDevicesData)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNewRequestStatus(deviceId: String, requestStatus: RequestStatus) {
|
||||
_dataMap[deviceId]?.update { it.copy(requestStatus = requestStatus) }
|
||||
}
|
||||
|
||||
suspend fun enableDistanceMode(deviceId: String, distanceMode: DistanceMode) {
|
||||
_dataMap[deviceId]?.update { it.copy(requestStatus = RequestStatus.PENDING) }
|
||||
DFSManager.enableDistanceMode(deviceId, distanceMode)
|
||||
}
|
||||
|
||||
private fun setDistanceMode(deviceId: String, distanceMode: DistanceMode) {
|
||||
_dataMap[deviceId]?.update { serviceData ->
|
||||
serviceData.copy(
|
||||
data = serviceData.data.mapValues { (key, sensorData) ->
|
||||
if (key == serviceData.selectedDevice) {
|
||||
sensorData.copy(distanceMode = distanceMode)
|
||||
} else {
|
||||
sensorData
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setAvailableDistanceModes(deviceId: String, ddfData: DDFData) {
|
||||
updateNewRequestStatus(deviceId, RequestStatus.PENDING)
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
ddfFeature = DDFData(
|
||||
isMcpdAvailable = ddfData.isMcpdAvailable,
|
||||
isRttAvailable = ddfData.isRttAvailable
|
||||
)
|
||||
)
|
||||
}
|
||||
updateNewRequestStatus(deviceId, RequestStatus.SUCCESS)
|
||||
}
|
||||
|
||||
fun onControlPointDataReceived(
|
||||
deviceId: String,
|
||||
data: ControlPointResult,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
when (data) {
|
||||
ControlPointChangeModeError -> {
|
||||
scope.launch {
|
||||
checkCurrentDistanceMode(deviceId)
|
||||
updateNewRequestStatus(deviceId, RequestStatus.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
is ControlPointChangeModeSuccess -> {
|
||||
scope.launch {
|
||||
setDistanceMode(deviceId, data.mode.toDistanceMode())
|
||||
updateNewRequestStatus(deviceId, RequestStatus.SUCCESS)
|
||||
}
|
||||
}
|
||||
|
||||
ControlPointCheckModeError -> {
|
||||
scope.launch {
|
||||
checkCurrentDistanceMode(deviceId)
|
||||
updateNewRequestStatus(deviceId, RequestStatus.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
is ControlPointCheckModeSuccess -> {
|
||||
scope.launch {
|
||||
setDistanceMode(deviceId, data.mode.toDistanceMode())
|
||||
updateNewRequestStatus(deviceId, RequestStatus.SUCCESS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun checkCurrentDistanceMode(deviceId: String) {
|
||||
updateNewRequestStatus(deviceId, RequestStatus.PENDING)
|
||||
DFSManager.checkForCurrentDistanceMode(deviceId)
|
||||
}
|
||||
|
||||
|
||||
fun updateDistanceRange(deviceId: String, range: Range) {
|
||||
_dataMap[deviceId]?.update { it.copy(distanceRange = range) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Update section to the sensor data.
|
||||
*/
|
||||
fun updateDetailsSection(deviceId: String, section: MeasurementSection) {
|
||||
_dataMap[deviceId]?.update { serviceData ->
|
||||
serviceData.copy(
|
||||
data = serviceData.data.mapValues { (key, sensorData) ->
|
||||
if (key == serviceData.selectedDevice) {
|
||||
sensorData.copy(selectedMeasurementSection = section)
|
||||
} else {
|
||||
sensorData
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkAvailableFeatures(deviceId: String) {
|
||||
DFSManager.checkAvailableFeatures(deviceId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSMeasurementContext
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSRecord
|
||||
import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.toolbox.profile.data.GLSServiceData
|
||||
import no.nordicsemi.android.toolbox.profile.manager.GLSManager
|
||||
|
||||
object GLSRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<GLSServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): StateFlow<GLSServiceData> = _dataMap.getOrPut(deviceId) {
|
||||
MutableStateFlow(GLSServiceData())
|
||||
}
|
||||
|
||||
fun updateNewRecord(deviceId: String, record: GLSRecord) {
|
||||
val records = _dataMap[deviceId]?.value?.records?.toMutableMap()
|
||||
records?.set(record, null)
|
||||
if (records != null) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
records = records.toMap()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWithNewContext(deviceId: String, context: GLSMeasurementContext) {
|
||||
val records = _dataMap[deviceId]?.value?.records?.toMutableMap()
|
||||
records?.keys?.firstOrNull { it.sequenceNumber == context.sequenceNumber }?.let {
|
||||
records[it] = context
|
||||
}
|
||||
if (records != null) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
records = records.toMap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) {
|
||||
clearState(deviceId)
|
||||
updateNewRequestStatus(deviceId, RequestStatus.PENDING)
|
||||
_dataMap[deviceId]?.update { it.copy(workingMode = workingMode) }
|
||||
GLSManager.requestRecord(deviceId, workingMode)
|
||||
}
|
||||
|
||||
fun updateNewRequestStatus(deviceId: String, requestStatus: RequestStatus) {
|
||||
_dataMap[deviceId]?.update { it.copy(requestStatus = requestStatus) }
|
||||
}
|
||||
|
||||
private fun clearState(deviceId: String) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
records = mapOf(),
|
||||
requestStatus = RequestStatus.IDLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.hrs.HRSData
|
||||
import no.nordicsemi.android.toolbox.profile.data.HRSServiceData
|
||||
|
||||
object HRSRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<HRSServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): Flow<HRSServiceData> {
|
||||
return _dataMap.getOrPut(deviceId) { MutableStateFlow(HRSServiceData()) }
|
||||
}
|
||||
|
||||
fun updateHRSData(deviceId: String, data: HRSData) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
heartRate = data.heartRate,
|
||||
data = it.data + data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
fun updateBodySensorLocation(deviceId: String, location: Int) {
|
||||
_dataMap[deviceId]?.update { it.copy(bodySensorLocation = location) }
|
||||
}
|
||||
|
||||
fun updateZoomIn(deviceId: String) {
|
||||
_dataMap[deviceId]?.update { it.copy(zoomIn = !it.zoomIn) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.hts.HTSData
|
||||
import no.nordicsemi.android.toolbox.profile.data.uiMapper.TemperatureUnit
|
||||
import no.nordicsemi.android.toolbox.profile.data.HTSServiceData
|
||||
|
||||
object HTSRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<HTSServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): Flow<HTSServiceData> {
|
||||
return _dataMap.getOrPut(deviceId) { MutableStateFlow(HTSServiceData()) }
|
||||
}
|
||||
|
||||
fun updateHTSData(deviceId: String, data: HTSData) {
|
||||
_dataMap[deviceId]?.update { it.copy(data = data) }
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
fun onTemperatureUnitChange(deviceId: String, unit: TemperatureUnit) {
|
||||
_dataMap[deviceId]?.update { it.copy(temperatureUnit = unit) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.data.LBSServiceData
|
||||
import no.nordicsemi.android.toolbox.profile.manager.LBSManager
|
||||
|
||||
data object LBSRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<LBSServiceData>>()
|
||||
|
||||
/**
|
||||
* Returns a [MutableStateFlow] that holds the [LBSServiceData] for the given device ID.
|
||||
* If no data exists for the device ID, it initializes a new [MutableStateFlow] with an empty [LBSServiceData].
|
||||
*/
|
||||
fun getData(deviceId: String): Flow<LBSServiceData> =
|
||||
_dataMap.getOrPut(deviceId) { MutableStateFlow(LBSServiceData()) }
|
||||
|
||||
/**
|
||||
* Updates the LED state for the given device ID.
|
||||
* If the device ID does not exist, it will not perform any action.
|
||||
*/
|
||||
fun updateLedState(deviceId: String, ledState: Boolean) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(data = it.data.copy(ledState = ledState))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the button state for the given device ID.
|
||||
* If the device ID does not exist, it will not perform any action.
|
||||
*/
|
||||
fun updateButtonState(deviceId: String, buttonState: Boolean) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(
|
||||
data = it.data.copy(
|
||||
buttonState = buttonState
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the data for the given device ID.
|
||||
* This will remove the [MutableStateFlow] associated with the device ID from the repository.
|
||||
*/
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
suspend fun writeToBlinkyLED(address: String, ledState: Boolean) {
|
||||
// Update the LED state for the given device address
|
||||
LBSManager.writeToBlinkyLED(deviceId = address, ledState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.prx.AlarmLevel
|
||||
import no.nordicsemi.android.toolbox.profile.parser.prx.PRXData
|
||||
|
||||
object PRXRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<PRXData>>()
|
||||
|
||||
fun getData(deviceId: String): Flow<PRXData> {
|
||||
return _dataMap.getOrPut(deviceId) { MutableStateFlow(PRXData()) }
|
||||
}
|
||||
|
||||
fun updatePRXData(deviceId: String, alarmLevel: AlarmLevel) {
|
||||
_dataMap[deviceId]?.update { it.copy(localAlarmLevel = alarmLevel) }
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
fun updateLinkLossAlarmLevelData(deviceId: String, linkLossAlarmLevel: AlarmLevel) {
|
||||
_dataMap[deviceId]?.update { it.copy(linkLossAlarmLevel = linkLossAlarmLevel) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCFeatureData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSData
|
||||
import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSSettingsUnit
|
||||
import no.nordicsemi.android.toolbox.profile.data.RSCSServiceData
|
||||
|
||||
object RSCSRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<RSCSServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): Flow<RSCSServiceData> {
|
||||
return _dataMap.getOrPut(deviceId) { MutableStateFlow(RSCSServiceData()) }
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
fun onRSCSDataChanged(deviceId: String, data: RSCSData) {
|
||||
_dataMap[deviceId]?.update { it.copy(data = data) }
|
||||
}
|
||||
|
||||
fun updateUnitSettings(deviceId: String, rscsUnitSettings: RSCSSettingsUnit) {
|
||||
_dataMap[deviceId]?.update { it.copy(unit = rscsUnitSettings) }
|
||||
}
|
||||
|
||||
fun updateRSCSFeatureData(deviceId: String, feature: RSCFeatureData) {
|
||||
_dataMap[deviceId]?.update { it.copy(feature = feature) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.parser.throughput.ThroughputMetrics
|
||||
import no.nordicsemi.android.toolbox.profile.data.ThroughputInputType
|
||||
import no.nordicsemi.android.toolbox.profile.data.ThroughputServiceData
|
||||
import no.nordicsemi.android.toolbox.profile.data.WritingStatus
|
||||
import no.nordicsemi.android.toolbox.profile.manager.ThroughputManager
|
||||
|
||||
object ThroughputRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<ThroughputServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): StateFlow<ThroughputServiceData> =
|
||||
_dataMap.getOrPut(deviceId) { MutableStateFlow(ThroughputServiceData()) }
|
||||
|
||||
fun updateThroughput(deviceId: String, throughputMetrics: ThroughputMetrics) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(throughputData = throughputMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendDataToDK(
|
||||
deviceId: String,
|
||||
writeDataType: ThroughputInputType,
|
||||
) {
|
||||
val maxWriteValueLength = _dataMap[deviceId]?.value?.maxWriteValueLength ?: 20
|
||||
ThroughputManager.writeRequest(
|
||||
deviceId = deviceId,
|
||||
maxWriteValueLength = maxWriteValueLength,
|
||||
inputType = writeDataType,
|
||||
)
|
||||
}
|
||||
|
||||
fun updateWriteStatus(deviceId: String, status: WritingStatus) {
|
||||
_dataMap[deviceId]?.update { it.copy(writingStatus = status) }
|
||||
}
|
||||
|
||||
fun updateMaxWriteValueLength(deviceId: String, mtuSize: Int?) {
|
||||
_dataMap[deviceId]?.update { it.copy(maxWriteValueLength = mtuSize) }
|
||||
}
|
||||
|
||||
fun clearData(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package no.nordicsemi.android.toolbox.profile.manager.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.toolbox.profile.data.UARTRecord
|
||||
import no.nordicsemi.android.toolbox.profile.data.UARTRecordType
|
||||
import no.nordicsemi.android.toolbox.profile.data.UARTServiceData
|
||||
import no.nordicsemi.android.toolbox.profile.data.uart.MacroEol
|
||||
import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration
|
||||
import no.nordicsemi.android.toolbox.profile.data.uart.UARTMacro
|
||||
import no.nordicsemi.android.toolbox.profile.data.uart.parseWithNewLineChar
|
||||
import no.nordicsemi.android.toolbox.profile.data.uart.toMacroEolUnicodeCharDisplay
|
||||
import no.nordicsemi.android.toolbox.profile.manager.UARTManager
|
||||
|
||||
object UartRepository {
|
||||
private val _dataMap = mutableMapOf<String, MutableStateFlow<UARTServiceData>>()
|
||||
|
||||
fun getData(deviceId: String): Flow<UARTServiceData> {
|
||||
return _dataMap.getOrPut(deviceId) { MutableStateFlow(UARTServiceData()) }
|
||||
}
|
||||
|
||||
fun updateMaxWriteLength(deviceId: String, maxWriteLength: Int) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(maxWriteLength = maxWriteLength)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNewMessageReceived(deviceId: String, message: String) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(messages = it.messages + UARTRecord(message, UARTRecordType.OUTPUT))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaxWriteLength(deviceId: String): Int {
|
||||
return _dataMap[deviceId]?.value?.maxWriteLength ?: 20
|
||||
}
|
||||
|
||||
fun onNewMessageSent(deviceId: String, message: String) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(messages = it.messages + UARTRecord(message, UARTRecordType.INPUT))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendText(deviceId: String, text: String, newLineChar: MacroEol) {
|
||||
if (_dataMap.containsKey(deviceId)) {
|
||||
UARTManager.sendText(
|
||||
deviceId,
|
||||
text.parseWithNewLineChar(newLineChar),
|
||||
getMaxWriteLength(deviceId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun runMacro(deviceId: String, macro: UARTMacro) {
|
||||
if (macro.command == null) return
|
||||
// Send the command to the device and update the command message.
|
||||
if (_dataMap.containsKey(deviceId)) {
|
||||
UARTManager.sendText(
|
||||
deviceId,
|
||||
macro.command!!.parseWithNewLineChar(macro.newLineChar),
|
||||
getMaxWriteLength(deviceId),
|
||||
macro.command!!.toMacroEolUnicodeCharDisplay(macro.newLineChar)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(deviceId: String) {
|
||||
_dataMap.remove(deviceId)
|
||||
}
|
||||
|
||||
fun clearOutputItems(deviceId: String) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(messages = emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteConfiguration(deviceId: String, configuration: UARTConfiguration) {
|
||||
_dataMap[deviceId]?.update {
|
||||
it.copy(uartViewState = it.uartViewState.copy(configurations = it.uartViewState.configurations - configuration))
|
||||
}
|
||||
}
|
||||
|
||||
fun addConfiguration(address: String, configuration: UARTConfiguration) {
|
||||
// Add the new configuration to the list
|
||||
_dataMap[address]?.update {
|
||||
val newConfig = configuration.copy(id = it.uartViewState.configurations.size + 1)
|
||||
it.copy(uartViewState = it.uartViewState.copy(configurations = it.uartViewState.configurations + newConfig))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelectedConfigurationName(address: String, configurationName: String) {
|
||||
_dataMap[address]?.update {
|
||||
it.copy(uartViewState = it.uartViewState.copy(selectedConfigurationName = configurationName))
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPreviousConfigurations(address: String, configuration: List<UARTConfiguration>) {
|
||||
_dataMap[address]?.update {
|
||||
it.copy(uartViewState = it.uartViewState.copy(configurations = configuration))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSelectedConfiguration(address: String) {
|
||||
_dataMap[address]?.update {
|
||||
it.copy(uartViewState = it.uartViewState.copy(selectedConfigurationName = null))
|
||||
}
|
||||
}
|
||||
|
||||
fun onEditConfiguration(address: String) {
|
||||
_dataMap[address]?.update {
|
||||
it.copy(uartViewState = it.uartViewState.copy(isConfigurationEdited = !it.uartViewState.isConfigurationEdited))
|
||||
}
|
||||
}
|
||||
|
||||
fun onEditMacro(address: String, editPosition: Int?) {
|
||||
_dataMap[address]?.update {
|
||||
it.copy(uartViewState = it.uartViewState.copy(editedPosition = editPosition))
|
||||
}
|
||||
}
|
||||
|
||||
fun addOrEditMacro(address: String, macro: UARTMacro): UARTConfiguration? {
|
||||
var newConfig: UARTConfiguration? = null
|
||||
_dataMap[address]?.update {
|
||||
it.uartViewState.selectedConfiguration?.let { selectedConfiguration ->
|
||||
val macros = selectedConfiguration.macros.toMutableList().apply {
|
||||
set(it.uartViewState.editedPosition!!, macro)
|
||||
}
|
||||
newConfig = selectedConfiguration.copy(macros = macros)
|
||||
// Save the new configuration and edited position.
|
||||
val newConfiguration = it.uartViewState.configurations.map { config ->
|
||||
if (config.id == selectedConfiguration.id) {
|
||||
newConfig!!
|
||||
} else {
|
||||
config
|
||||
}
|
||||
}
|
||||
it.copy(
|
||||
uartViewState = it.uartViewState.copy(
|
||||
configurations = newConfiguration,
|
||||
editedPosition = null
|
||||
)
|
||||
)
|
||||
}!!
|
||||
}
|
||||
return newConfig
|
||||
}
|
||||
|
||||
fun onEditFinished(address: String) {
|
||||
_dataMap[address]?.update {
|
||||
it.copy(uartViewState = it.uartViewState.copy(editedPosition = null))
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteMacro(address: String) {
|
||||
_dataMap[address]?.update {
|
||||
it.uartViewState.selectedConfiguration?.let { selectedConfiguration ->
|
||||
val macros = selectedConfiguration.macros.toMutableList().apply {
|
||||
set(it.uartViewState.editedPosition!!, null)
|
||||
}
|
||||
val newConfig = selectedConfiguration.copy(macros = macros)
|
||||
|
||||
// Save the new configuration and edited position.
|
||||
val newConfiguration = it.uartViewState.configurations.map { config ->
|
||||
if (config.id == selectedConfiguration.id) {
|
||||
newConfig
|
||||
} else {
|
||||
config
|
||||
}
|
||||
}
|
||||
it.copy(
|
||||
uartViewState = it.uartViewState.copy(
|
||||
configurations = newConfiguration,
|
||||
editedPosition = null
|
||||
)
|
||||
)
|
||||
}!!
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user