Add BPS & RSCS profiles

This commit is contained in:
Sylwester Zieliński
2021-10-14 17:32:39 +02:00
parent 5a84cc495b
commit 2a28d7b255
39 changed files with 1300 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#00B3DC"
android:pathData="M773.3,436.5c0,0 -0.1,-0.1 -0.1,-0.1L513,17.6c-5.2,-8.4 -14.4,-13.5 -24.2,-13.5s-19,5.1 -24.2,13.5L204.4,436.4c0,0 -0.1,0.1 -0.1,0.1c-20,32.6 -66.5,117.2 -66.5,198.7c0,46 9.3,90.6 27.7,132.6c17.7,40.5 43.1,76.8 75.4,108c66.3,63.9 154.3,99.1 247.8,99.1c93.6,0 181.6,-35.2 247.8,-99.1c32.3,-31.2 57.7,-67.5 75.4,-108c18.4,-42 27.7,-86.6 27.7,-132.6C839.8,553.7 793.3,469.1 773.3,436.5zM488.8,917.8c-162.1,0 -294,-126.8 -294,-282.6c0,-68.8 44.4,-146.5 58,-168.8l236,-379.8l236,379.8c13.7,22.3 58,100.1 58,168.8C782.8,791 650.9,917.8 488.8,917.8z" />
<path
android:fillColor="#00B3DC"
android:pathData="M405.2,423.6c-1.5,-2.2 -3.8,-2.2 -5.3,0.1l-53.4,81.7c-2.5,3.8 -0.8,10.5 2.7,10.5h35v246.4c0,10.5 8.5,19 19,19s19,-8.5 19,-19V515.9h35c3.5,0 5.2,-6.8 2.6,-10.6L405.2,423.6z" />
<path
android:fillColor="#00B3DC"
android:pathData="M628.3,696.8h-35V450.4c0,-10.5 -8.5,-19 -19,-19s-19,8.5 -19,19v246.4h-35c-3.5,0 -5.2,6.8 -2.6,10.6l54.7,81.7c1.5,2.2 3.8,2.2 5.3,-0.1l53.4,-81.7C633.5,703.5 631.8,696.8 628.3,696.8z" />
</vector>

View File

@@ -0,0 +1,7 @@
<vector android:height="80dp" android:viewportHeight="1024"
android:viewportWidth="1024" android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00B3DC" android:pathData="M982.9,706.2c-10.8,-37.3 -61.8,-59.1 -126.3,-86.7c-26.1,-11.2 -53.1,-22.7 -73.8,-34.3c-26.6,-14.9 -30.8,-23.3 -31.4,-24.8C730,504.6 641.7,260.1 634.7,237.4c-6.8,-22.1 -23.1,-38.5 -43.7,-43.8c-12.9,-3.3 -38.5,-5 -65.7,21.3c-16.6,16 -30.5,38.5 -40.2,57.1c-4.8,-3.8 -9.9,-7.9 -15,-12.3c-37.1,-31.1 -43.9,-44 -44.7,-46c-1.7,-8.8 -0.2,-25.9 1,-41c2.7,-31.8 5.2,-61.8 -11.7,-80.3c-15,-16.5 -41.3,-23.4 -72.2,-19.1c-31.8,4.4 -67.1,19.7 -105,45.4c-19.8,13.4 -36.3,27 -50,39.8c-0.6,0.5 -1.2,1.1 -1.8,1.7c-0.1,0.1 -0.3,0.3 -0.4,0.4c-14.2,13.4 -25.2,26 -33.6,36.8c-36.3,42.6 -64,85 -79.2,109.6c-14.6,23.8 -17.6,52.5 -8.3,78.8c9.5,26.7 30.1,66.5 74.6,103.9c91,76.7 198.5,176.9 227.5,227.9c19.9,35.1 48.2,80.3 90.2,118.7c52.8,48.3 114.7,74.2 184,77c38.5,1.6 77.8,2.8 115,2.8c33,0 64.3,-1 92,-3.7c50.6,-4.9 118.8,-17.4 127.9,-69c2.8,-15.7 4.7,-37.3 6.1,-57.8C984.7,758.2 987.6,722.4 982.9,706.2zM269.4,165.8c58,-39.4 91.9,-38.7 101.4,-35.5c1.1,8.3 -0.4,26.3 -1.4,37.6c-1.7,20.3 -3.5,41.4 0.1,58.3c2.8,13.1 12.3,33.5 61.1,74.9c24.4,20.7 47.7,37.4 48.7,38.1c7.2,5.1 16.3,6.6 24.7,4.1c8.4,-2.5 15.2,-8.8 18.4,-17c5.2,-13.3 22.9,-51.9 42.3,-70.6c6.8,-6.6 11.1,-7.3 11.9,-7.1c0.5,0.1 2.4,1.7 3.5,5.3c3.1,10.2 18.4,53.4 37.2,105.6l-73.1,12.9c-10.3,1.8 -17.2,11.7 -15.4,22c1.6,9.2 9.6,15.7 18.7,15.7c1.1,0 2.2,-0.1 3.3,-0.3l79.6,-14.1c5.3,14.8 10.8,29.9 16.2,44.7l-74.3,13.1c-10.3,1.8 -17.2,11.7 -15.4,22c1.6,9.2 9.6,15.7 18.7,15.7c1.1,0 2.2,-0.1 3.3,-0.3l80.9,-14.3c5.8,15.8 11.3,31 16.4,44.7L600.4,535c-10.3,1.8 -17.2,11.7 -15.4,22c1.6,9.2 9.6,15.7 18.7,15.7c1.1,0 2.2,-0.1 3.3,-0.3l82.6,-14.6c3.5,9.5 6.5,17.3 8.6,22.9c7.4,19.4 25.5,36.6 56.7,54.1c23.4,13.1 51.8,25.3 79.3,37c24.5,10.5 47.7,20.4 66.1,30.4c19.3,10.4 25.5,17 27.4,19.3c-0.1,0.5 -0.1,1.1 -0.2,1.6c-0.1,1.4 -0.2,3.5 -0.4,8.6c-0.3,6.3 -0.7,15.8 -1.3,26.9c-63.1,9.8 -182.8,18.5 -260.2,2.1c-98.2,-20.7 -146.9,-106.7 -179.2,-163.6l-1.5,-2.7C448.5,530.4 305.1,389 239.1,333.4c-40.1,-33.8 -51.7,-67.3 -55.1,-83.3c2.4,-3.9 6,-9.4 11.1,-16.1c9.5,-11.2 19.7,-22.4 30.4,-33.2C237.3,189.7 251.8,177.8 269.4,165.8zM841.8,855.7c-55.5,5.4 -128.5,3.5 -199.2,0.6c-119.9,-4.8 -184.2,-91.7 -226.9,-166.9c-40,-70.4 -181.4,-193.6 -240.3,-243.3c-34.7,-29.2 -50.5,-59.3 -57.6,-79.4c-3.5,-10 -2.4,-20.9 3.1,-29.9c5.6,-9.1 13,-20.7 22,-33.9c13.3,26.9 33.7,52.2 59.5,74c70.8,59.7 203.3,193.6 233,245.7l1.5,2.7c16.9,29.8 40.1,70.7 73.9,107.2c41,44.3 89.2,72.6 143.1,84c35.8,7.6 77.7,10.3 119.1,10.3c55.7,0 110.6,-5 148.4,-10.1c-0.7,6.2 -1.5,11.8 -2.3,16.7C919,833.7 912,848.9 841.8,855.7z"/>
<path android:fillColor="#00B3DC" android:pathData="M54.9,666.5h87.5c10.5,0 19,-8.5 19,-19s-8.5,-19 -19,-19H54.9c-10.5,0 -19,8.5 -19,19S44.4,666.5 54.9,666.5z"/>
<path android:fillColor="#00B3DC" android:pathData="M54.9,782.2H239c10.5,0 19,-8.5 19,-19s-8.5,-19 -19,-19H54.9c-10.5,0 -19,8.5 -19,19S44.4,782.2 54.9,782.2z"/>
<path android:fillColor="#00B3DC" android:pathData="M364.3,859.9H54.9c-10.5,0 -19,8.5 -19,19s8.5,19 19,19h309.4c10.5,0 19,-8.5 19,-19S374.8,859.9 364.3,859.9z"/>
</vector>

View File

@@ -3,4 +3,6 @@
<string name="hrs_module">HRS</string>
<string name="gls_module">GLS</string>
<string name="hts_module">HTS</string>
<string name="bps_module">BPS</string>
<string name="rscs_module">RSCS</string>
</resources>

26
profile_bps/build.gradle Normal file
View File

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

View File

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

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="no.nordicsemi.android.bps">
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package no.nordicsemi.android.bps.view
internal sealed class BPSScreenViewEvent
internal object DisconnectEvent : BPSScreenViewEvent()

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="bps_title">Blood pressure</string>
<string name="bps_systolic">Systolic</string>
<string name="bps_diastolic">Diastolic</string>
<string name="bps_mean">Mean ap</string>
<string name="bps_pulse">Pulse</string>
<string name="bps_time_data">Time and Date</string>
</resources>

View File

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

26
profile_rscs/build.gradle Normal file
View File

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

View File

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

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="no.nordicsemi.android.rscs">
<application>
<service android:name=".service.RSCSService"/>
</application>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package no.nordicsemi.android.rscs.view
internal sealed class RSCScreenViewEvent
internal object DisconnectEvent : RSCScreenViewEvent()

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="rscs_title">Running speed &amp; cadence</string>
<string name="rscs_activity">Activity</string>
<string name="rscs_pace">Pace</string>
<string name="rscs_cadence">Cadence</string>
<string name="rscs_number_of_steps">Number of steps</string>
</resources>

View File

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

View File

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