mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-02-19 05:34:20 +01:00
Add BPS & RSCS profiles
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
15
app/src/main/res/drawable/ic_bps.xml
Normal file
15
app/src/main/res/drawable/ic_bps.xml
Normal 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>
|
||||
7
app/src/main/res/drawable/ic_rscs.xml
Normal file
7
app/src/main/res/drawable/ic_rscs.xml
Normal 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>
|
||||
@@ -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
26
profile_bps/build.gradle
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
5
profile_bps/src/main/AndroidManifest.xml
Normal file
5
profile_bps/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.bps.view
|
||||
|
||||
internal sealed class BPSScreenViewEvent
|
||||
|
||||
internal object DisconnectEvent : BPSScreenViewEvent()
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
10
profile_bps/src/main/res/values/strings.xml
Normal file
10
profile_bps/src/main/res/values/strings.xml
Normal 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>
|
||||
@@ -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
26
profile_rscs/build.gradle
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
8
profile_rscs/src/main/AndroidManifest.xml
Normal file
8
profile_rscs/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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()) { }
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.rscs.view
|
||||
|
||||
internal sealed class RSCScreenViewEvent
|
||||
|
||||
internal object DisconnectEvent : RSCScreenViewEvent()
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
9
profile_rscs/src/main/res/values/strings.xml
Normal file
9
profile_rscs/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="rscs_title">Running speed & 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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user