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:
Himali Aryal
2025-07-30 14:51:02 +02:00
committed by GitHub
parent 9a71e66c10
commit b67abd60e6
513 changed files with 19164 additions and 14446 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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")
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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
)
}

View File

@@ -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()
}
}

View File

@@ -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}")
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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) }
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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()) }
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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) }
}
}

View File

@@ -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) }
}
}

View File

@@ -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)
}
}

View File

@@ -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) }
}
}

View File

@@ -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) }
}
}

View File

@@ -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)
}
}

View File

@@ -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
)
)
}!!
}
}
}