diff --git a/app/build.gradle b/app/build.gradle
index 060252d2..bda0ea86 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -50,10 +50,12 @@ android {
dependencies {
//Hilt requires to implement every module in the main app module
//https://github.com/google/dagger/issues/2123
+ implementation project(':profile_bps')
implementation project(':profile_csc')
implementation project(':profile_hrs')
implementation project(':profile_hts')
implementation project(':profile_gls')
+ implementation project(':profile_rscs')
implementation project(':profile_permission')
implementation project(':profile_scanner')
implementation project(":lib_theme")
diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt
index 723f3419..e8e10137 100644
--- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt
+++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt
@@ -25,6 +25,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
+import no.nordicsemi.android.bps.view.BPSScreen
import no.nordicsemi.android.csc.view.CSCScreen
import no.nordicsemi.android.gls.view.GLSScreen
import no.nordicsemi.android.hrs.view.HRSScreen
@@ -32,6 +33,7 @@ import no.nordicsemi.android.hts.view.HTSScreen
import no.nordicsemi.android.permission.view.BluetoothNotAvailableScreen
import no.nordicsemi.android.permission.view.BluetoothNotEnabledScreen
import no.nordicsemi.android.permission.view.RequestPermissionScreen
+import no.nordicsemi.android.rscs.view.RSCSScreen
import no.nordicsemi.android.scanner.view.ScanDeviceScreen
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
import no.nordicsemi.android.theme.view.CloseIconAppBar
@@ -53,6 +55,8 @@ internal fun HomeScreen() {
composable(NavDestination.HRS.id) { HRSScreen { viewModel.navigateUp() } }
composable(NavDestination.HTS.id) { HTSScreen { viewModel.navigateUp() } }
composable(NavDestination.GLS.id) { GLSScreen { viewModel.navigateUp() } }
+ composable(NavDestination.BPS.id) { BPSScreen { viewModel.navigateUp() } }
+ composable(NavDestination.RSCS.id) { RSCSScreen { viewModel.navigateUp() } }
composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen{ viewModel.finish() } }
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
@@ -91,6 +95,10 @@ fun HomeView(callback: (NavDestination) -> Unit) {
FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) }
Spacer(modifier = Modifier.height(1.dp))
FeatureButton(R.drawable.ic_hts, R.string.hts_module) { callback(NavDestination.HTS) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_bps, R.string.bps_module) { callback(NavDestination.BPS) }
+ Spacer(modifier = Modifier.height(1.dp))
+ FeatureButton(R.drawable.ic_rscs, R.string.rscs_module) { callback(NavDestination.RSCS) }
}
}
diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt
index 144ac172..9b28ebcf 100644
--- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt
+++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt
@@ -8,6 +8,8 @@ enum class NavDestination(val id: String) {
HRS("hrs-screen"),
HTS("hts-screen"),
GLS("gls-screen"),
+ BPS("bps-screen"),
+ RSCS("rscs-screen"),
REQUEST_PERMISSION("request-permission"),
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"),
diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt
index f6419e15..1cbac2ca 100644
--- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt
+++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt
@@ -3,6 +3,7 @@ package no.nordicsemi.android.nrftoolbox
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
+import no.nordicsemi.android.bps.repository.BPS_SERVICE_UUID
import no.nordicsemi.android.csc.service.CYCLING_SPEED_AND_CADENCE_SERVICE_UUID
import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID
import no.nordicsemi.android.hrs.service.HR_SERVICE_UUID
@@ -11,6 +12,7 @@ import no.nordicsemi.android.permission.tools.NordicBleScanner
import no.nordicsemi.android.permission.tools.PermissionHelper
import no.nordicsemi.android.permission.tools.ScannerStatus
import no.nordicsemi.android.permission.viewmodel.BluetoothPermissionState
+import no.nordicsemi.android.rscs.service.RSCS_SERVICE_UUID
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
import javax.inject.Inject
@@ -73,6 +75,8 @@ class NavigationViewModel @Inject constructor(
NavDestination.HRS -> HR_SERVICE_UUID.toString()
NavDestination.HTS -> HT_SERVICE_UUID.toString()
NavDestination.GLS -> GLS_SERVICE_UUID.toString()
+ NavDestination.BPS -> BPS_SERVICE_UUID.toString()
+ NavDestination.RSCS -> RSCS_SERVICE_UUID.toString()
NavDestination.HOME,
NavDestination.REQUEST_PERMISSION,
NavDestination.BLUETOOTH_NOT_AVAILABLE,
diff --git a/app/src/main/res/drawable/ic_bps.xml b/app/src/main/res/drawable/ic_bps.xml
new file mode 100644
index 00000000..583afc04
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bps.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_rscs.xml b/app/src/main/res/drawable/ic_rscs.xml
new file mode 100644
index 00000000..a79bb78b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_rscs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4188a864..4ed85901 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,4 +3,6 @@
HRS
GLS
HTS
+ BPS
+ RSCS
\ No newline at end of file
diff --git a/profile_bps/build.gradle b/profile_bps/build.gradle
new file mode 100644
index 00000000..d397c91b
--- /dev/null
+++ b/profile_bps/build.gradle
@@ -0,0 +1,26 @@
+apply from: rootProject.file("library.gradle")
+apply plugin: 'kotlin-parcelize'
+
+dependencies {
+ implementation project(":lib_service")
+ implementation project(":lib_theme")
+ implementation project(":lib_utils")
+
+ implementation libs.nordic.ble.common
+
+ implementation libs.nordic.log
+
+ implementation libs.bundles.compose
+ implementation libs.androidx.core
+ implementation libs.material
+ implementation libs.lifecycle.activity
+ implementation libs.lifecycle.service
+ implementation libs.compose.lifecycle
+ implementation libs.compose.activity
+
+ testImplementation libs.test.junit
+ androidTestImplementation libs.android.test.junit
+ androidTestImplementation libs.android.test.espresso
+ androidTestImplementation libs.android.test.compose.ui
+ debugImplementation libs.android.test.compose.tooling
+}
diff --git a/profile_bps/src/androidTest/java/no/nordicsemi/android/bps/ExampleInstrumentedTest.kt b/profile_bps/src/androidTest/java/no/nordicsemi/android/bps/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..5dd697c4
--- /dev/null
+++ b/profile_bps/src/androidTest/java/no/nordicsemi/android/bps/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package no.nordicsemi.android.bps
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("no.nordicsemi.android.bps.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/profile_bps/src/main/AndroidManifest.xml b/profile_bps/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..457baeb5
--- /dev/null
+++ b/profile_bps/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSData.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSData.kt
new file mode 100644
index 00000000..22ce1791
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSData.kt
@@ -0,0 +1,38 @@
+package no.nordicsemi.android.bps.data
+
+import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes
+import java.util.*
+
+data class BPSData(
+ val batteryLevel: Int = 0,
+ val cuffPressure: Float = 0f,
+ val unit: Int = 0,
+ val pulseRate: Float? = null,
+ val userID: Int? = null,
+ val status: BloodPressureTypes.BPMStatus? = null,
+ val calendar: Calendar? = null,
+ val systolic: Float = 0f,
+ val diastolic: Float = 0f,
+ val meanArterialPressure: Float = 0f,
+) {
+
+ fun displaySystolic(): String {
+ return "$systolic"
+ }
+
+ fun displayDiastolic(): String {
+ return "$diastolic"
+ }
+
+ fun displayMeanArterialPressure(): String {
+ return "$meanArterialPressure"
+ }
+
+ fun displayPulse(): String {
+ return "$pulseRate"
+ }
+
+ fun displayTimeData(): String {
+ return ""
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSDataHolder.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSDataHolder.kt
new file mode 100644
index 00000000..04015ca1
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSDataHolder.kt
@@ -0,0 +1,63 @@
+package no.nordicsemi.android.bps.data
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes
+import java.util.*
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class BPSDataHolder @Inject constructor() {
+
+ private val _data = MutableStateFlow(BPSData())
+ val data: StateFlow = _data
+
+ fun setIntermediateCuffPressure(
+ cuffPressure: Float,
+ unit: Int,
+ pulseRate: Float?,
+ userID: Int?,
+ status: BloodPressureTypes.BPMStatus?,
+ calendar: Calendar?
+ ) {
+ _data.tryEmit(_data.value.copy(
+ cuffPressure = cuffPressure,
+ unit = unit,
+ pulseRate = pulseRate,
+ userID = userID,
+ status = status,
+ calendar = calendar
+ ))
+ }
+
+ fun setBloodPressureMeasurement(
+ systolic: Float,
+ diastolic: Float,
+ meanArterialPressure: Float,
+ unit: Int,
+ pulseRate: Float?,
+ userID: Int?,
+ status: BloodPressureTypes.BPMStatus?,
+ calendar: Calendar?
+ ) {
+ _data.tryEmit(_data.value.copy(
+ systolic = systolic,
+ diastolic = diastolic,
+ meanArterialPressure = meanArterialPressure,
+ unit = unit,
+ pulseRate = pulseRate,
+ userID = userID,
+ status = status,
+ calendar = calendar
+ ))
+ }
+
+ fun setBatteryLevel(batteryLevel: Int) {
+ _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
+ }
+
+ fun clear() {
+ _data.tryEmit(BPSData())
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt
new file mode 100644
index 00000000..a932b0b4
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.bps.repository
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCharacteristic
+import android.content.Context
+import android.util.Log
+import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementDataCallback
+import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureDataCallback
+import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes
+import no.nordicsemi.android.ble.data.Data
+import no.nordicsemi.android.bps.data.BPSDataHolder
+import no.nordicsemi.android.log.LogContract
+import no.nordicsemi.android.service.BatteryManager
+import java.util.*
+
+/** Blood Pressure service UUID. */
+val BPS_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
+
+/** Blood Pressure Measurement characteristic UUID. */
+private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
+
+/** Intermediate Cuff Pressure characteristic UUID. */
+private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb")
+
+internal class BPSManager(context: Context, private val dataHolder: BPSDataHolder) : BatteryManager(context) {
+
+ private var bpmCharacteristic: BluetoothGattCharacteristic? = null
+ private var icpCharacteristic: BluetoothGattCharacteristic? = null
+
+ private val intermediateCuffPressureCallback = object : IntermediateCuffPressureDataCallback() {
+
+ override fun onDataReceived(device: BluetoothDevice, data: Data) {
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + IntermediateCuffPressureParser.parse(data)
+ .toString() + "\" received"
+ )
+
+ // Pass through received data
+ super.onDataReceived(device, data)
+ }
+
+ override fun onIntermediateCuffPressureReceived(
+ device: BluetoothDevice,
+ cuffPressure: Float,
+ unit: Int,
+ pulseRate: Float?,
+ userID: Int?,
+ status: BloodPressureTypes.BPMStatus?,
+ calendar: Calendar?
+ ) {
+ dataHolder.setIntermediateCuffPressure(
+ cuffPressure,
+ unit,
+ pulseRate,
+ userID,
+ status,
+ calendar
+ )
+ }
+
+ override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
+ log(Log.WARN, "Invalid ICP data received: $data")
+ }
+ }
+
+ private val bloodPressureMeasurementDataCallback = object : BloodPressureMeasurementDataCallback() {
+
+ override fun onDataReceived(device: BluetoothDevice, data: Data) {
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + BloodPressureMeasurementParser.parse(data)
+ .toString() + "\" received"
+ )
+
+ // Pass through received data
+ super.onDataReceived(device, data)
+ }
+
+ override fun onBloodPressureMeasurementReceived(
+ device: BluetoothDevice,
+ systolic: Float,
+ diastolic: Float,
+ meanArterialPressure: Float,
+ unit: Int,
+ pulseRate: Float?,
+ userID: Int?,
+ status: BloodPressureTypes.BPMStatus?,
+ calendar: Calendar?
+ ) {
+ dataHolder.setBloodPressureMeasurement(
+ systolic,
+ diastolic,
+ meanArterialPressure,
+ unit,
+ pulseRate,
+ userID,
+ status,
+ calendar
+ )
+ }
+
+ override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
+ log(Log.WARN, "Invalid BPM data received: $data")
+ }
+ }
+
+ override fun onBatteryLevelChanged(batteryLevel: Int) {
+ dataHolder.setBatteryLevel(batteryLevel)
+ }
+
+ /**
+ * BluetoothGatt callbacks for connection/disconnection, service discovery,
+ * receiving notification, etc.
+ */
+ private inner class BloodPressureManagerGattCallback : BatteryManagerGattCallback() {
+
+ override fun initialize() {
+ super.initialize()
+ setNotificationCallback(icpCharacteristic)
+ .with(intermediateCuffPressureCallback)
+ setIndicationCallback(bpmCharacteristic)
+ .with(bloodPressureMeasurementDataCallback)
+ enableNotifications(icpCharacteristic)
+ .fail { device, status ->
+ log(
+ Log.WARN,
+ "Intermediate Cuff Pressure characteristic not found"
+ )
+ }
+ .enqueue()
+ enableIndications(bpmCharacteristic).enqueue()
+ }
+
+ override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
+ val service = gatt.getService(BPS_SERVICE_UUID)
+ if (service != null) {
+ bpmCharacteristic = service.getCharacteristic(BPM_CHARACTERISTIC_UUID)
+ icpCharacteristic = service.getCharacteristic(ICP_CHARACTERISTIC_UUID)
+ }
+ return bpmCharacteristic != null
+ }
+
+ override fun onServicesInvalidated() { }
+
+ override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
+ super.isOptionalServiceSupported(gatt) // ignore the result of this
+ return icpCharacteristic != null
+ }
+
+ override fun onDeviceDisconnected() {
+ icpCharacteristic = null
+ bpmCharacteristic = null
+ }
+ }
+
+ override fun getGattCallback(): BleManagerGattCallback {
+ return BloodPressureManagerGattCallback()
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BloodPressureMeasurementParser.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BloodPressureMeasurementParser.kt
new file mode 100644
index 00000000..ff92a7ae
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BloodPressureMeasurementParser.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.bps.repository
+
+import no.nordicsemi.android.ble.data.Data
+import no.nordicsemi.android.bps.repository.DateTimeParser.parse
+import java.lang.StringBuilder
+
+object BloodPressureMeasurementParser {
+
+ fun parse(data: Data): String {
+ val builder = StringBuilder()
+
+ // first byte - flags
+ var offset = 0
+ val flags = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ val unitType = flags and 0x01
+ val timestampPresent = flags and 0x02 > 0
+ val pulseRatePresent = flags and 0x04 > 0
+ val userIdPresent = flags and 0x08 > 0
+ val statusPresent = flags and 0x10 > 0
+
+ // following bytes - systolic, diastolic and mean arterial pressure
+ val systolic = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ val diastolic = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 2)!!
+ val meanArterialPressure = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 4)!!
+ val unit = if (unitType == 0) " mmHg" else " kPa"
+ offset += 6
+ builder.append("Systolic: ").append(systolic).append(unit)
+ builder.append("\nDiastolic: ").append(diastolic).append(unit)
+ builder.append("\nMean AP: ").append(meanArterialPressure).append(unit)
+
+ // parse timestamp if present
+ if (timestampPresent) {
+ builder.append("\nTimestamp: ").append(parse(data, offset))
+ offset += 7
+ }
+
+ // parse pulse rate if present
+ if (pulseRatePresent) {
+ val pulseRate = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ offset += 2
+ builder.append("\nPulse: ").append(pulseRate).append(" bpm")
+ }
+ if (userIdPresent) {
+ val userId = data.getIntValue(Data.FORMAT_UINT8, offset)!!
+ offset += 1
+ builder.append("\nUser ID: ").append(userId)
+ }
+ if (statusPresent) {
+ val status = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ // offset += 2;
+ if (status and 0x0001 > 0) builder.append("\nBody movement detected")
+ if (status and 0x0002 > 0) builder.append("\nCuff too lose")
+ if (status and 0x0004 > 0) builder.append("\nIrregular pulse detected")
+ if (status and 0x0018 == 0x0008) builder.append("\nPulse rate exceeds upper limit")
+ if (status and 0x0018 == 0x0010) builder.append("\nPulse rate is less than lower limit")
+ if (status and 0x0018 == 0x0018) builder.append("\nPulse rate range: Reserved for future use ")
+ if (status and 0x0020 > 0) builder.append("\nImproper measurement position")
+ }
+ return builder.toString()
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/DateTimeParser.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/DateTimeParser.kt
new file mode 100644
index 00000000..7e38af1f
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/DateTimeParser.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.bps.repository
+
+import no.nordicsemi.android.ble.common.callback.DateTimeDataCallback
+import no.nordicsemi.android.ble.data.Data
+import java.util.*
+
+object DateTimeParser {
+ /**
+ * Parses the date and time info.
+ *
+ * @param data
+ * @return time in human readable format
+ */
+ fun parse(data: Data): String {
+ return parse(data, 0)
+ }
+
+ /**
+ * Parses the date and time info. This data has 7 bytes
+ *
+ * @param data
+ * @param offset
+ * offset to start reading the time
+ * @return time in human readable format
+ */
+ /* package */
+ @JvmStatic
+ fun parse(data: Data, offset: Int): String {
+ val calendar = DateTimeDataCallback.readDateTime(data, offset)
+ return String.format(Locale.US, "%1\$te %1\$tb %1\$tY, %1\$tH:%1\$tM:%1\$tS", calendar)
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/IntermediateCuffPressureParser.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/IntermediateCuffPressureParser.kt
new file mode 100644
index 00000000..6b9bbb24
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/IntermediateCuffPressureParser.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.bps.repository
+
+import no.nordicsemi.android.ble.data.Data
+import no.nordicsemi.android.bps.repository.DateTimeParser.parse
+import java.lang.StringBuilder
+
+object IntermediateCuffPressureParser {
+
+ fun parse(data: Data): String {
+ val builder = StringBuilder()
+
+ // first byte - flags
+ var offset = 0
+ val flags = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
+ val unitType = flags and 0x01
+ val timestampPresent = flags and 0x02 > 0
+ val pulseRatePresent = flags and 0x04 > 0
+ val userIdPresent = flags and 0x08 > 0
+ val statusPresent = flags and 0x10 > 0
+
+ // following bytes - pressure
+ val pressure = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ val unit = if (unitType == 0) " mmHg" else " kPa"
+ offset += 6
+ builder.append("Cuff pressure: ").append(pressure).append(unit)
+
+ // parse timestamp if present
+ if (timestampPresent) {
+ builder.append("Timestamp: ").append(parse(data, offset))
+ offset += 7
+ }
+
+ // parse pulse rate if present
+ if (pulseRatePresent) {
+ val pulseRate = data.getFloatValue(Data.FORMAT_SFLOAT, offset)!!
+ offset += 2
+ builder.append("\nPulse: ").append(pulseRate).append(" bpm")
+ }
+ if (userIdPresent) {
+ val userId = data.getIntValue(Data.FORMAT_UINT8, offset)!!
+ offset += 1
+ builder.append("\nUser ID: ").append(userId)
+ }
+ if (statusPresent) {
+ val status = data.getIntValue(Data.FORMAT_UINT16, offset)!!
+ // offset += 2;
+ if (status and 0x0001 > 0) builder.append("\nBody movement detected")
+ if (status and 0x0002 > 0) builder.append("\nCuff too lose")
+ if (status and 0x0004 > 0) builder.append("\nIrregular pulse detected")
+ if (status and 0x0018 == 0x0008) builder.append("\nPulse rate exceeds upper limit")
+ if (status and 0x0018 == 0x0010) builder.append("\nPulse rate is less than lower limit")
+ if (status and 0x0018 == 0x0018) builder.append("\nPulse rate range: Reserved for future use ")
+ if (status and 0x0020 > 0) builder.append("\nImproper measurement position")
+ }
+ return builder.toString()
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt
new file mode 100644
index 00000000..1bb585a2
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt
@@ -0,0 +1,36 @@
+package no.nordicsemi.android.bps.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.bps.R
+import no.nordicsemi.android.bps.data.BPSData
+
+@Composable
+internal fun BPSContentView(state: BPSData, onEvent: (BPSScreenViewEvent) -> Unit) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+
+ BPSSensorsReadingView(state = state)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary),
+ onClick = { onEvent(DisconnectEvent) }
+ ) {
+ Text(text = stringResource(id = R.string.disconnect))
+ }
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt
new file mode 100644
index 00000000..a0b50c54
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt
@@ -0,0 +1,31 @@
+package no.nordicsemi.android.bps.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.res.stringResource
+import androidx.hilt.navigation.compose.hiltViewModel
+import no.nordicsemi.android.bps.R
+import no.nordicsemi.android.bps.data.BPSData
+import no.nordicsemi.android.bps.viewmodel.BPSViewModel
+import no.nordicsemi.android.theme.view.BackIconAppBar
+
+@Composable
+fun BPSScreen(finishAction: () -> Unit) {
+ val viewModel: BPSViewModel = hiltViewModel()
+ val state = viewModel.state.collectAsState().value
+ val isScreenActive = viewModel.isActive.collectAsState().value
+
+ BPSView(state) { viewModel.onEvent(it) }
+}
+
+@Composable
+private fun BPSView(state: BPSData, onEvent: (BPSScreenViewEvent) -> Unit) {
+ Column {
+ BackIconAppBar(stringResource(id = R.string.bps_title)) {
+ onEvent(DisconnectEvent)
+ }
+
+ BPSContentView(state) { onEvent(it) }
+ }
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreenViewEvent.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreenViewEvent.kt
new file mode 100644
index 00000000..445a07e8
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreenViewEvent.kt
@@ -0,0 +1,5 @@
+package no.nordicsemi.android.bps.view
+
+internal sealed class BPSScreenViewEvent
+
+internal object DisconnectEvent : BPSScreenViewEvent()
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt
new file mode 100644
index 00000000..722a3921
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt
@@ -0,0 +1,42 @@
+package no.nordicsemi.android.bps.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.bps.R
+import no.nordicsemi.android.bps.data.BPSData
+import no.nordicsemi.android.theme.view.BatteryLevelView
+import no.nordicsemi.android.theme.view.KeyValueField
+import no.nordicsemi.android.theme.view.ScreenSection
+
+@Composable
+internal fun BPSSensorsReadingView(state: BPSData) {
+ ScreenSection {
+ Column {
+ KeyValueField(stringResource(id = R.string.bps_systolic), state.displaySystolic())
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(stringResource(id = R.string.bps_diastolic), state.displayDiastolic())
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(stringResource(id = R.string.bps_mean), state.displayMeanArterialPressure())
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(stringResource(id = R.string.bps_pulse), state.displayPulse())
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(stringResource(id = R.string.bps_time_data), state.displayTimeData())
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ BatteryLevelView(state.batteryLevel)
+}
+
+@Preview
+@Composable
+private fun Preview() {
+ BPSSensorsReadingView(BPSData())
+}
diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt
new file mode 100644
index 00000000..afbc2c0a
--- /dev/null
+++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt
@@ -0,0 +1,28 @@
+package no.nordicsemi.android.bps.viewmodel
+
+import dagger.hilt.android.lifecycle.HiltViewModel
+import no.nordicsemi.android.bps.data.BPSDataHolder
+import no.nordicsemi.android.bps.view.BPSScreenViewEvent
+import no.nordicsemi.android.bps.view.DisconnectEvent
+import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
+import no.nordicsemi.android.utils.exhaustive
+import javax.inject.Inject
+
+@HiltViewModel
+internal class BPSViewModel @Inject constructor(
+ private val dataHolder: BPSDataHolder
+) : CloseableViewModel() {
+
+ val state = dataHolder.data
+
+ fun onEvent(event: BPSScreenViewEvent) {
+ when (event) {
+ DisconnectEvent -> onDisconnectButtonClick()
+ }.exhaustive
+ }
+
+ private fun onDisconnectButtonClick() {
+ finish()
+ dataHolder.clear()
+ }
+}
diff --git a/profile_bps/src/main/res/values/strings.xml b/profile_bps/src/main/res/values/strings.xml
new file mode 100644
index 00000000..e9b7392a
--- /dev/null
+++ b/profile_bps/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+ Blood pressure
+
+ Systolic
+ Diastolic
+ Mean ap
+ Pulse
+ Time and Date
+
diff --git a/profile_bps/src/test/java/no/nordicsemi/android/bps/ExampleUnitTest.kt b/profile_bps/src/test/java/no/nordicsemi/android/bps/ExampleUnitTest.kt
new file mode 100644
index 00000000..6621f261
--- /dev/null
+++ b/profile_bps/src/test/java/no/nordicsemi/android/bps/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package no.nordicsemi.android.bps
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/profile_rscs/build.gradle b/profile_rscs/build.gradle
new file mode 100644
index 00000000..d397c91b
--- /dev/null
+++ b/profile_rscs/build.gradle
@@ -0,0 +1,26 @@
+apply from: rootProject.file("library.gradle")
+apply plugin: 'kotlin-parcelize'
+
+dependencies {
+ implementation project(":lib_service")
+ implementation project(":lib_theme")
+ implementation project(":lib_utils")
+
+ implementation libs.nordic.ble.common
+
+ implementation libs.nordic.log
+
+ implementation libs.bundles.compose
+ implementation libs.androidx.core
+ implementation libs.material
+ implementation libs.lifecycle.activity
+ implementation libs.lifecycle.service
+ implementation libs.compose.lifecycle
+ implementation libs.compose.activity
+
+ testImplementation libs.test.junit
+ androidTestImplementation libs.android.test.junit
+ androidTestImplementation libs.android.test.espresso
+ androidTestImplementation libs.android.test.compose.ui
+ debugImplementation libs.android.test.compose.tooling
+}
diff --git a/profile_rscs/src/androidTest/java/no/nordicsemi/android/rscs/ExampleInstrumentedTest.kt b/profile_rscs/src/androidTest/java/no/nordicsemi/android/rscs/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..33bfc46c
--- /dev/null
+++ b/profile_rscs/src/androidTest/java/no/nordicsemi/android/rscs/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package no.nordicsemi.android.rscs
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("no.nordicsemi.android.rscs.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/profile_rscs/src/main/AndroidManifest.xml b/profile_rscs/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..896ad954
--- /dev/null
+++ b/profile_rscs/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSData.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSData.kt
new file mode 100644
index 00000000..6f2d8ce1
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSData.kt
@@ -0,0 +1,37 @@
+package no.nordicsemi.android.rscs.data
+
+internal data class RSCSData(
+ val batteryLevel: Int = 0,
+ val running: Boolean = false,
+ val instantaneousSpeed: Float = 1.0f,
+ val instantaneousCadence: Int = 0,
+ val strideLength: Int? = null,
+ val totalDistance: Long? = null
+) {
+
+ fun displayActivity(): String {
+ return if (running) {
+ "Running"
+ } else {
+ "Walking"
+ }
+ }
+
+ fun displayPace(): String {
+ return "$instantaneousCadence min/km"
+ }
+
+
+ fun displayCadence(): String {
+ return "$instantaneousCadence RPM"
+ }
+
+
+ fun displayNumberOfSteps(): String {
+ if (totalDistance == null || strideLength == null) {
+ return "NONE"
+ }
+ val numberOfSteps = totalDistance/strideLength
+ return "Number of Steps $numberOfSteps"
+ }
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSDataHolder.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSDataHolder.kt
new file mode 100644
index 00000000..6ca05ddb
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSDataHolder.kt
@@ -0,0 +1,37 @@
+package no.nordicsemi.android.rscs.data
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class RSCSDataHolder @Inject constructor() {
+
+ private val _data = MutableStateFlow(RSCSData())
+ val data: StateFlow = _data
+
+ fun setNewData(
+ running: Boolean,
+ instantaneousSpeed: Float,
+ instantaneousCadence: Int,
+ strideLength: Int?,
+ totalDistance: Long?
+ ) {
+ _data.tryEmit(_data.value.copy(
+ running = running,
+ instantaneousCadence = instantaneousCadence,
+ instantaneousSpeed = instantaneousSpeed,
+ strideLength = strideLength,
+ totalDistance = totalDistance
+ ))
+ }
+
+ fun setBatteryLevel(batteryLevel: Int) {
+ _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
+ }
+
+ fun clear() {
+ _data.tryEmit(RSCSData())
+ }
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt
new file mode 100644
index 00000000..d0246d2c
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.rscs.service
+
+import no.nordicsemi.android.ble.data.Data
+import java.util.*
+
+internal object RSCMeasurementParser {
+
+ private const val INSTANTANEOUS_STRIDE_LENGTH_PRESENT: Byte = 0x01 // 1 bit
+ private const val TOTAL_DISTANCE_PRESENT: Byte = 0x02 // 1 bit
+ private const val WALKING_OR_RUNNING_STATUS_BITS: Byte = 0x04 // 1 bit
+
+ fun parse(data: Data): String {
+ var offset = 0
+ val flags = data.value!![offset].toInt() // 1 byte
+ offset += 1
+ val islmPresent = flags and INSTANTANEOUS_STRIDE_LENGTH_PRESENT.toInt() > 0
+ val tdPreset = flags and TOTAL_DISTANCE_PRESENT.toInt() > 0
+ val running = flags and WALKING_OR_RUNNING_STATUS_BITS.toInt() > 0
+ val walking = !running
+ val instantaneousSpeed =
+ data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 256.0f // 1/256 m/s
+ offset += 2
+ val instantaneousCadence = data.getIntValue(Data.FORMAT_UINT8, offset)!!
+ offset += 1
+ var instantaneousStrideLength = 0f
+ if (islmPresent) {
+ instantaneousStrideLength =
+ data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 100.0f // 1/100 m
+ offset += 2
+ }
+ var totalDistance = 0f
+ if (tdPreset) {
+ totalDistance = data.getIntValue(Data.FORMAT_UINT32, offset) as Float / 10.0f
+ // offset += 4;
+ }
+ val builder = StringBuilder()
+ builder.append(
+ String.format(
+ Locale.US,
+ "Speed: %.2f m/s, Cadence: %d RPM,\n",
+ instantaneousSpeed,
+ instantaneousCadence
+ )
+ )
+ if (islmPresent) builder.append(
+ String.format(
+ Locale.US,
+ "Instantaneous Stride Length: %.2f m,\n",
+ instantaneousStrideLength
+ )
+ )
+ if (tdPreset) builder.append(
+ String.format(
+ Locale.US,
+ "Total Distance: %.1f m,\n",
+ totalDistance
+ )
+ )
+ if (walking) builder.append("Status: WALKING") else builder.append("Status: RUNNING")
+ return builder.toString()
+ }
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt
new file mode 100644
index 00000000..84c0a680
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2015, Nordic Semiconductor
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package no.nordicsemi.android.rscs.service
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCharacteristic
+import android.content.Context
+import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback
+import no.nordicsemi.android.ble.data.Data
+import no.nordicsemi.android.log.LogContract
+import no.nordicsemi.android.rscs.data.RSCSDataHolder
+import no.nordicsemi.android.service.BatteryManager
+import java.util.*
+
+/** Running Speed and Cadence Measurement service UUID */
+val RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805f9b34fb")
+
+/** Running Speed and Cadence Measurement characteristic UUID */
+private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-0000-1000-8000-00805f9b34fb")
+
+internal class RSCSManager internal constructor(
+ context: Context,
+ private val dataHolder: RSCSDataHolder
+) : BatteryManager(context) {
+
+ private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
+
+ private val callback = object : RunningSpeedAndCadenceMeasurementDataCallback() {
+ override fun onDataReceived(device: BluetoothDevice, data: Data) {
+ log(
+ LogContract.Log.Level.APPLICATION,
+ "\"" + RSCMeasurementParser.parse(data).toString() + "\" received"
+ )
+ super.onDataReceived(device, data)
+ }
+
+ override fun onRSCMeasurementReceived(
+ device: BluetoothDevice,
+ running: Boolean,
+ instantaneousSpeed: Float,
+ instantaneousCadence: Int,
+ strideLength: Int?,
+ totalDistance: Long?
+ ) {
+ dataHolder.setNewData(running, instantaneousSpeed, instantaneousCadence, strideLength, totalDistance)
+ }
+ }
+
+ override fun onBatteryLevelChanged(batteryLevel: Int) {
+ dataHolder.setBatteryLevel(batteryLevel)
+ }
+
+ override fun getGattCallback(): BatteryManagerGattCallback {
+ return RSCManagerGattCallback()
+ }
+
+ /**
+ * BluetoothGatt callbacks for connection/disconnection, service discovery,
+ * receiving indication, etc.
+ */
+ private inner class RSCManagerGattCallback : BatteryManagerGattCallback() {
+
+ override fun initialize() {
+ super.initialize()
+ setNotificationCallback(rscMeasurementCharacteristic)
+ .with(callback)
+ enableNotifications(rscMeasurementCharacteristic).enqueue()
+ }
+
+ public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
+ val service = gatt.getService(RSCS_SERVICE_UUID)
+ if (service != null) {
+ rscMeasurementCharacteristic = service.getCharacteristic(
+ RSC_MEASUREMENT_CHARACTERISTIC_UUID
+ )
+ }
+ return rscMeasurementCharacteristic != null
+ }
+
+ override fun onServicesInvalidated() {
+
+ }
+
+ override fun onDeviceDisconnected() {
+ super.onDeviceDisconnected()
+ rscMeasurementCharacteristic = null
+ }
+ }
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSService.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSService.kt
new file mode 100644
index 00000000..70317bcf
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSService.kt
@@ -0,0 +1,15 @@
+package no.nordicsemi.android.rscs.service
+
+import dagger.hilt.android.AndroidEntryPoint
+import no.nordicsemi.android.rscs.data.RSCSDataHolder
+import no.nordicsemi.android.service.ForegroundBleService
+import javax.inject.Inject
+
+@AndroidEntryPoint
+internal class RSCSService : ForegroundBleService() {
+
+ @Inject
+ lateinit var dataHolder: RSCSDataHolder
+
+ override val manager: RSCSManager by lazy { RSCSManager(this, dataHolder) }
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSContentView.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSContentView.kt
new file mode 100644
index 00000000..0238961a
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSContentView.kt
@@ -0,0 +1,43 @@
+package no.nordicsemi.android.rscs.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.rscs.R
+import no.nordicsemi.android.rscs.data.RSCSData
+
+@Composable
+internal fun RSCSContentView(state: RSCSData, onEvent: (RSCScreenViewEvent) -> Unit) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+
+ SensorsReadingView(state = state)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary),
+ onClick = { onEvent(DisconnectEvent) }
+ ) {
+ Text(text = stringResource(id = R.string.disconnect))
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun RSCSContentViewPreview() {
+ RSCSContentView(RSCSData()) { }
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt
new file mode 100644
index 00000000..67ff8b11
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt
@@ -0,0 +1,54 @@
+package no.nordicsemi.android.rscs.view
+
+import android.content.Intent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.hilt.navigation.compose.hiltViewModel
+import no.nordicsemi.android.rscs.R
+import no.nordicsemi.android.rscs.data.RSCSData
+import no.nordicsemi.android.rscs.service.RSCSService
+import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
+import no.nordicsemi.android.theme.view.BackIconAppBar
+import no.nordicsemi.android.utils.isServiceRunning
+
+@Composable
+fun RSCSScreen(finishAction: () -> Unit) {
+ val viewModel: RSCSViewModel = hiltViewModel()
+ val state = viewModel.state.collectAsState().value
+ val isScreenActive = viewModel.isActive.collectAsState().value
+
+ val context = LocalContext.current
+ LaunchedEffect(isScreenActive) {
+ if (!isScreenActive) {
+ finishAction()
+ }
+ if (context.isServiceRunning(RSCSService::class.java.name)) {
+ val intent = Intent(context, RSCSService::class.java)
+ context.stopService(intent)
+ }
+ }
+
+ LaunchedEffect("start-service") {
+ if (!context.isServiceRunning(RSCSService::class.java.name)) {
+ val intent = Intent(context, RSCSService::class.java)
+ context.startService(intent)
+ }
+ }
+
+ RSCSView(state) { viewModel.onEvent(it) }
+}
+
+@Composable
+private fun RSCSView(state: RSCSData, onEvent: (RSCScreenViewEvent) -> Unit) {
+ Column {
+ BackIconAppBar(stringResource(id = R.string.rscs_title)) {
+ onEvent(DisconnectEvent)
+ }
+
+ RSCSContentView(state) { onEvent(it) }
+ }
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCScreenViewEvent.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCScreenViewEvent.kt
new file mode 100644
index 00000000..f759ff10
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCScreenViewEvent.kt
@@ -0,0 +1,5 @@
+package no.nordicsemi.android.rscs.view
+
+internal sealed class RSCScreenViewEvent
+
+internal object DisconnectEvent : RSCScreenViewEvent()
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt
new file mode 100644
index 00000000..7b8507e1
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt
@@ -0,0 +1,43 @@
+package no.nordicsemi.android.rscs.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.rscs.R
+import no.nordicsemi.android.rscs.data.RSCSData
+import no.nordicsemi.android.theme.view.BatteryLevelView
+import no.nordicsemi.android.theme.view.KeyValueField
+import no.nordicsemi.android.theme.view.ScreenSection
+
+@Composable
+internal fun SensorsReadingView(state: RSCSData) {
+ ScreenSection {
+ Column {
+ KeyValueField(stringResource(id = R.string.rscs_activity), state.displayActivity())
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(stringResource(id = R.string.rscs_pace), state.displayPace())
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(stringResource(id = R.string.rscs_cadence), state.displayCadence())
+ Spacer(modifier = Modifier.height(4.dp))
+ KeyValueField(
+ stringResource(id = R.string.rscs_number_of_steps),
+ state.displayNumberOfSteps()
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ BatteryLevelView(state.batteryLevel)
+}
+
+@Preview
+@Composable
+private fun Preview() {
+ SensorsReadingView(RSCSData())
+}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt
new file mode 100644
index 00000000..f088831f
--- /dev/null
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt
@@ -0,0 +1,28 @@
+package no.nordicsemi.android.rscs.viewmodel
+
+import dagger.hilt.android.lifecycle.HiltViewModel
+import no.nordicsemi.android.rscs.data.RSCSDataHolder
+import no.nordicsemi.android.rscs.view.DisconnectEvent
+import no.nordicsemi.android.rscs.view.RSCScreenViewEvent
+import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
+import no.nordicsemi.android.utils.exhaustive
+import javax.inject.Inject
+
+@HiltViewModel
+internal class RSCSViewModel @Inject constructor(
+ private val dataHolder: RSCSDataHolder
+) : CloseableViewModel() {
+
+ val state = dataHolder.data
+
+ fun onEvent(event: RSCScreenViewEvent) {
+ when (event) {
+ DisconnectEvent -> onDisconnectButtonClick()
+ }.exhaustive
+ }
+
+ private fun onDisconnectButtonClick() {
+ finish()
+ dataHolder.clear()
+ }
+}
diff --git a/profile_rscs/src/main/res/values/strings.xml b/profile_rscs/src/main/res/values/strings.xml
new file mode 100644
index 00000000..e02bc07d
--- /dev/null
+++ b/profile_rscs/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+ Running speed & cadence
+
+ Activity
+ Pace
+ Cadence
+ Number of steps
+
diff --git a/profile_rscs/src/test/java/no/nordicsemi/android/rscs/ExampleUnitTest.kt b/profile_rscs/src/test/java/no/nordicsemi/android/rscs/ExampleUnitTest.kt
new file mode 100644
index 00000000..a5629c50
--- /dev/null
+++ b/profile_rscs/src/test/java/no/nordicsemi/android/rscs/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package no.nordicsemi.android.rscs
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 91b53830..b361e1f2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -62,10 +62,12 @@ rootProject.name = "Android-nRF-Toolbox"
include ':app'
+include ':profile_bps'
include ':profile_csc'
include ':profile_gls'
include ':profile_hrs'
include ':profile_hts'
+include ':profile_rscs'
include ':profile_permission'
include ':lib_service'