Add HTS module

This commit is contained in:
Sylwester Zieliński
2021-10-04 11:14:08 +02:00
parent b2da2f20eb
commit 0384b717b6
36 changed files with 736 additions and 45 deletions

View File

@@ -52,6 +52,7 @@ dependencies {
//https://github.com/google/dagger/issues/2123
implementation project(":feature_csc")
implementation project(":feature_hrs")
implementation project(":feature_hts")
implementation project(":feature_gls")
implementation project(':feature_scanner')
implementation project(":lib_theme")

View File

@@ -22,6 +22,7 @@ import androidx.navigation.compose.rememberNavController
import no.nordicsemi.android.csc.view.CscScreen
import no.nordicsemi.android.gls.view.GLSScreen
import no.nordicsemi.android.hrs.view.HRSScreen
import no.nordicsemi.android.hts.view.HTSScreen
import no.nordicsemi.android.scanner.view.BluetoothNotAvailableScreen
import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen
import no.nordicsemi.android.scanner.view.RequestPermissionScreen
@@ -43,6 +44,7 @@ internal fun HomeScreen() {
composable(NavDestination.HOME.id) { HomeView { viewModel.navigate(it) } }
composable(NavDestination.CSC.id) { CscScreen { viewModel.navigateUp() } }
composable(NavDestination.HRS.id) { HRSScreen { viewModel.navigateUp() } }
composable(NavDestination.HTS.id) { HTSScreen { viewModel.navigateUp() } }
composable(NavDestination.GLS.id) { GLSScreen { viewModel.navigateUp() } }
composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen() }

View File

@@ -4,6 +4,7 @@ enum class NavDestination(val id: String) {
HOME("home-screen"),
CSC("csc-screen"),
HRS("hrs-screen"),
HTS("hts-screen"),
GLS("gls-screen"),
REQUEST_PERMISSION("request-permission"),
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),

View File

@@ -0,0 +1,21 @@
<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="M813.8,338h-92.9c-15.7,0 -28.5,12.8 -28.5,28.5s12.8,28.5 28.5,28.5h92.9c15.7,0 28.5,-12.8 28.5,-28.5S829.6,338 813.8,338z" />
<path
android:fillColor="#00B3DC"
android:pathData="M720.9,240.7h92.9c15.7,0 28.5,-12.8 28.5,-28.5s-12.8,-28.5 -28.5,-28.5h-92.9c-15.7,0 -28.5,12.8 -28.5,28.5S705.2,240.7 720.9,240.7z" />
<path
android:fillColor="#00B3DC"
android:pathData="M813.8,492.3h-92.9c-15.7,0 -28.5,12.8 -28.5,28.5c0,15.7 12.8,28.5 28.5,28.5h92.9c15.7,0 28.5,-12.8 28.5,-28.5C842.3,505.1 829.6,492.3 813.8,492.3z" />
<path
android:fillColor="#00B3DC"
android:pathData="M637.5,604.9V175.3c0,-66.8 -54.3,-121.1 -121.1,-121.1s-121.1,54.3 -121.1,121.1v429.6c-18.2,15.5 -33.5,34.6 -44.6,55.8c-13.9,26.5 -21.2,56.4 -21.2,86.5c0,103 83.8,186.8 186.8,186.8c103,0 186.8,-83.8 186.8,-186.8c0,-30.1 -7.3,-60 -21.2,-86.5C671,639.5 655.7,620.5 637.5,604.9zM516.4,877c-71.6,0 -129.8,-58.2 -129.8,-129.8c0,-41.6 20.2,-80.9 53.9,-105.3c7.4,-5.4 11.8,-14 11.8,-23.1V175.3c0,-35.3 28.7,-64.1 64.1,-64.1c35.3,0 64.1,28.7 64.1,64.1v443.4c0,9.2 4.4,17.8 11.8,23.1c33.8,24.4 53.9,63.8 53.9,105.3C646.2,818.8 588,877 516.4,877z" />
<path
android:fillColor="#00B3DC"
android:pathData="M601.3,747c0,-1.3 0,-2.7 -0.1,-4c0,-0.4 -0.1,-0.8 -0.1,-1.2c-0.1,-0.9 -0.1,-1.8 -0.2,-2.8c0,-0.5 -0.1,-0.9 -0.1,-1.4c-0.1,-0.9 -0.2,-1.8 -0.3,-2.7c-0.1,-0.4 -0.1,-0.8 -0.2,-1.2c-0.2,-1.3 -0.4,-2.6 -0.7,-3.8c0,0 0,-0.1 0,-0.1c-0.3,-1.2 -0.5,-2.4 -0.8,-3.6c-0.1,-0.4 -0.2,-0.8 -0.3,-1.2c-0.2,-0.9 -0.5,-1.7 -0.7,-2.5c-0.1,-0.4 -0.3,-0.9 -0.4,-1.3c-0.3,-0.8 -0.5,-1.7 -0.8,-2.5c-0.1,-0.4 -0.3,-0.8 -0.4,-1.1c-0.4,-1.2 -0.9,-2.3 -1.4,-3.5c0,-0.1 -0.1,-0.2 -0.1,-0.2c-0.5,-1.1 -0.9,-2.1 -1.4,-3.2c-0.2,-0.4 -0.4,-0.8 -0.6,-1.1c-0.4,-0.8 -0.8,-1.5 -1.2,-2.2c-0.2,-0.4 -0.4,-0.8 -0.7,-1.2c-0.4,-0.7 -0.8,-1.5 -1.3,-2.2c-0.2,-0.4 -0.4,-0.7 -0.6,-1.1c-0.6,-1 -1.3,-2 -1.9,-3c-0.1,-0.2 -0.2,-0.3 -0.3,-0.5c-0.6,-0.9 -1.2,-1.8 -1.9,-2.6c-0.3,-0.4 -0.5,-0.7 -0.8,-1.1c-0.5,-0.6 -1,-1.3 -1.5,-1.9c-0.3,-0.4 -0.6,-0.8 -0.9,-1.1c-0.5,-0.6 -1,-1.2 -1.6,-1.8c-0.3,-0.3 -0.6,-0.7 -0.9,-1c-0.8,-0.8 -1.5,-1.7 -2.3,-2.5c-0.2,-0.2 -0.4,-0.4 -0.6,-0.6c-0.7,-0.7 -1.4,-1.4 -2.2,-2.1c-0.4,-0.3 -0.7,-0.6 -1.1,-0.9c-0.6,-0.5 -1.2,-1 -1.7,-1.5c-0.4,-0.3 -0.8,-0.7 -1.2,-1c-0.6,-0.5 -1.2,-1 -1.8,-1.4c-0.4,-0.3 -0.7,-0.6 -1.1,-0.9c-0.9,-0.6 -1.8,-1.3 -2.7,-1.9c-0.3,-0.2 -0.6,-0.4 -1,-0.6c-0.8,-0.5 -1.6,-1 -2.4,-1.5c-0.4,-0.3 -0.9,-0.5 -1.3,-0.8c-0.6,-0.4 -1.3,-0.8 -2,-1.1c-0.5,-0.3 -0.9,-0.5 -1.4,-0.8c-0.7,-0.4 -1.4,-0.7 -2,-1c-0.4,-0.2 -0.9,-0.4 -1.3,-0.7c-1,-0.5 -2,-0.9 -3,-1.4c-0.4,-0.2 -0.8,-0.3 -1.1,-0.5c-0.9,-0.4 -1.7,-0.7 -2.6,-1c-0.5,-0.2 -1,-0.3 -1.4,-0.5c-0.4,-0.2 -0.9,-0.3 -1.3,-0.5V334.1l-53.8,0.8v331.6c-0.4,0.1 -0.9,0.3 -1.3,0.5c-0.5,0.2 -1,0.3 -1.4,0.5c-0.9,0.3 -1.8,0.7 -2.6,1c-0.4,0.2 -0.8,0.3 -1.1,0.5c-1,0.4 -2,0.9 -3,1.4c-0.4,0.2 -0.9,0.4 -1.3,0.7c-0.7,0.3 -1.4,0.7 -2,1c-0.5,0.2 -0.9,0.5 -1.4,0.8c-0.7,0.4 -1.3,0.7 -2,1.1c-0.4,0.3 -0.9,0.5 -1.3,0.8c-0.8,0.5 -1.6,1 -2.4,1.5c-0.3,0.2 -0.6,0.4 -1,0.6c-0.9,0.6 -1.8,1.3 -2.7,1.9c-0.4,0.3 -0.8,0.6 -1.1,0.9c-0.6,0.5 -1.2,0.9 -1.8,1.4c-0.4,0.3 -0.8,0.6 -1.2,1c-0.6,0.5 -1.2,1 -1.7,1.5c-0.4,0.3 -0.7,0.6 -1.1,0.9c-0.7,0.7 -1.5,1.4 -2.2,2.1c-0.2,0.2 -0.4,0.4 -0.6,0.6c-0.8,0.8 -1.6,1.6 -2.3,2.5c-0.3,0.3 -0.6,0.7 -0.9,1c-0.5,0.6 -1.1,1.2 -1.6,1.8c-0.3,0.4 -0.6,0.7 -0.9,1.1c-0.5,0.6 -1,1.3 -1.5,1.9c-0.3,0.4 -0.5,0.7 -0.8,1.1c-0.6,0.9 -1.3,1.7 -1.9,2.6c-0.1,0.2 -0.2,0.3 -0.3,0.5c-0.7,1 -1.3,2 -1.9,3c-0.2,0.4 -0.4,0.7 -0.6,1.1c-0.4,0.7 -0.8,1.4 -1.3,2.2c-0.2,0.4 -0.4,0.8 -0.7,1.2c-0.4,0.7 -0.8,1.5 -1.2,2.2c-0.2,0.4 -0.4,0.8 -0.6,1.1c-0.5,1.1 -1,2.1 -1.4,3.2c0,0.1 -0.1,0.2 -0.1,0.2c-0.5,1.2 -0.9,2.3 -1.4,3.5c-0.1,0.4 -0.3,0.8 -0.4,1.1c-0.3,0.8 -0.6,1.7 -0.8,2.5c-0.1,0.4 -0.3,0.9 -0.4,1.3c-0.3,0.8 -0.5,1.7 -0.7,2.5c-0.1,0.4 -0.2,0.8 -0.3,1.2c-0.3,1.2 -0.6,2.4 -0.8,3.6c0,0 0,0.1 0,0.1c-0.3,1.3 -0.5,2.5 -0.7,3.8c-0.1,0.4 -0.1,0.8 -0.2,1.2c-0.1,0.9 -0.2,1.8 -0.3,2.7c0,0.5 -0.1,0.9 -0.1,1.4c-0.1,0.9 -0.2,1.8 -0.2,2.8c0,0.4 -0.1,0.8 -0.1,1.2c-0.1,1.3 -0.1,2.7 -0.1,4c0,0 0,0 0,0v0c0,0 0,0 0,0c0,20.5 7.3,39.3 19.4,54c15.6,18.9 39.1,30.9 65.5,30.9c26.4,0 49.9,-12 65.5,-30.9C594,786.3 601.3,767.5 601.3,747C601.3,747 601.3,747 601.3,747L601.3,747C601.3,747 601.3,747 601.3,747z" />
</vector>

View File

@@ -55,4 +55,8 @@ internal data class CSCData(
fun displayGearRatio(): String {
return String.format(Locale.US, "%.1f", gearRatio)
}
fun items(): List<> {
}
}

View File

@@ -4,10 +4,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -18,10 +16,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.theme.NordicColors
import no.nordicsemi.android.theme.view.SensorRecordCard
@Composable
internal fun ContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
if (state.showDialog) {
SelectWheelSizeDialog { onEvent(it) }
}
@@ -49,11 +47,7 @@ internal fun ContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
@Composable
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
Card(
backgroundColor = NordicColors.NordicGray4.value(),
shape = RoundedCornerShape(10.dp),
elevation = 0.dp
) {
SensorRecordCard {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -70,5 +64,5 @@ private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
@Preview
@Composable
private fun ConnectedPreview() {
ContentView(CSCData()) { }
CSCContentView(CSCData()) { }
}

View File

@@ -47,6 +47,6 @@ private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) })
ContentView(state) { onEvent(it) }
CSCContentView(state) { onEvent(it) }
}
}

View File

@@ -4,8 +4,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -13,17 +11,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.theme.NordicColors
import no.nordicsemi.android.theme.view.BatteryLevelView
import no.nordicsemi.android.theme.view.KeyValueField
import no.nordicsemi.android.theme.view.SensorRecordCard
@Composable
internal fun SensorsReadingView(state: CSCData) {
Card(
backgroundColor = NordicColors.NordicGray4.value(),
shape = RoundedCornerShape(10.dp),
elevation = 0.dp
) {
SensorRecordCard {
Column(modifier = Modifier.padding(16.dp)) {
KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed())
Spacer(modifier = Modifier.height(4.dp))

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
@@ -14,32 +13,33 @@ import androidx.compose.ui.unit.dp
@Composable
internal fun SpeedUnitRadioGroup(
currentUnit: SpeedUnit,
onEvent: (OnSelectedSpeedUnitSelected) -> Unit
currentItem: RadioGroupItem,
items: List<RadioGroupItem>,
onEvent: (RadioGroupItem) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
SpeedUnitRadioButton(currentUnit, SpeedUnit.KM_H, onEvent)
SpeedUnitRadioButton(currentUnit, SpeedUnit.MPH, onEvent)
SpeedUnitRadioButton(currentUnit, SpeedUnit.M_S, onEvent)
items.forEach {
SpeedUnitRadioButton(currentItem, it, onEvent)
}
}
}
@Composable
internal fun SpeedUnitRadioButton(
selectedUnit: SpeedUnit,
displayedUnit: SpeedUnit,
onEvent: (OnSelectedSpeedUnitSelected) -> Unit
selectedItem: RadioGroupItem,
displayedItem: RadioGroupItem,
onEvent: (RadioGroupItem) -> Unit
) {
Row {
RadioButton(
selected = (selectedUnit == displayedUnit),
onClick = { onEvent(OnSelectedSpeedUnitSelected(displayedUnit)) }
selected = (selectedItem == displayedItem),
onClick = { onEvent(displayedItem) }
)
Spacer(modifier = Modifier.width(4.dp))
Text(text = createSpeedUnitLabel(displayedUnit))
Text(text = displayedItem.label)
}
}
@@ -50,3 +50,5 @@ internal fun createSpeedUnitLabel(unit: SpeedUnit): String {
SpeedUnit.MPH -> "mph"
}
}
data class RadioGroupItem(val label: String)

View File

@@ -3,7 +3,8 @@ package no.nordicsemi.android.gls.data
internal data class GLSData(
val record: List<GLSRecord> = emptyList(),
val batteryLevel: Int = 0,
val requestStatus: RequestStatus = RequestStatus.IDLE
val requestStatus: RequestStatus = RequestStatus.IDLE,
val isDeviceBonded: Boolean = false
)
internal enum class RequestStatus {

View File

@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
@@ -16,6 +17,10 @@ import no.nordicsemi.android.gls.viewmodel.GLSViewModel
fun GLSScreen(finishAction: () -> Unit) {
val viewModel: GLSViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
LaunchedEffect(state.isDeviceBonded) {
// viewModel.bondDevice()
}
}
@Composable

View File

@@ -15,6 +15,10 @@ internal class GLSViewModel @Inject constructor(
val state = glsManager.data
fun bondDevice() {
if (deviceHolder.isDeviceBonded()) {
deviceHolder.bondDevice()
} else {
//start work
}
}
}

View File

@@ -23,7 +23,7 @@ package no.nordicsemi.android.hrs.service
import no.nordicsemi.android.ble.data.Data
object BodySensorLocationParser {
internal object BodySensorLocationParser {
fun parse(data: Data): String {
val value = data.getIntValue(Data.FORMAT_UINT8, 0)!!
return when (value) {
@@ -37,4 +37,4 @@ object BodySensorLocationParser {
else -> "Other"
}
}
}
}

View File

@@ -24,7 +24,7 @@ package no.nordicsemi.android.hrs.service
import no.nordicsemi.android.ble.data.Data
import java.util.*
object HeartRateMeasurementParser {
internal object HeartRateMeasurementParser {
private const val HEART_RATE_VALUE_FORMAT: Byte = 0x01 // 1 bit
private const val SENSOR_CONTACT_STATUS: Byte = 0x06 // 2 bits

View File

@@ -38,7 +38,7 @@ import no.nordicsemi.android.theme.view.BatteryLevelView
import java.util.*
@Composable
internal fun ContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) {
internal fun HRSContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
@@ -220,5 +220,5 @@ private fun updateData(points: List<Int>, chart: LineChart) {
@Preview
@Composable
private fun Preview() {
ContentView(state = HRSViewState()) { }
HRSContentView(state = HRSViewState()) { }
}

View File

@@ -47,6 +47,6 @@ private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit)
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.hrs_title)) })
ContentView(state) { onEvent(it) }
HRSContentView(state) { onEvent(it) }
}
}

26
feature_hts/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.hts
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.hts.test", appContext.packageName)
}
}

View File

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

View File

@@ -0,0 +1,22 @@
package no.nordicsemi.android.hts.data
internal data class HTSData(
val heartRates: List<Int> = emptyList(),
val temperature: Temperature = Temperature.CELSIUS,
val batteryLevel: Int = 0,
val sensorLocation: Int = 0,
val isScreenActive: Boolean = true
) {
fun displayTemperature() {
val value = when (temperature) {
Temperature.CELSIUS -> TODO()
Temperature.FAHRENHEIT -> TODO()
Temperature.KELVIN -> TODO()
}
}
}
internal enum class Temperature {
CELSIUS, FAHRENHEIT, KELVIN
}

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.hts.service
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,9 @@
package no.nordicsemi.android.hts.service
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.service.BluetoothDataReadBroadcast
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class HTSDataBroadcast @Inject constructor() : BluetoothDataReadBroadcast<HTSData>()

View File

@@ -0,0 +1,103 @@
/*
* 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.hts.service
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCallback
import no.nordicsemi.android.ble.common.profile.ht.TemperatureType
import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.service.BatteryManager
import java.util.*
private val HT_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb")
private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb")
/**
* [HTSManager] class performs [BluetoothGatt] operations for connection, service discovery,
* enabling indication and reading characteristics. All operations required to connect to device
* with BLE HT Service and reading health thermometer values are performed here.
*/
class HTSManager internal constructor(context: Context) : BatteryManager<HTSManagerCallbacks>(context) {
private var htCharacteristic: BluetoothGattCharacteristic? = null
override fun getGattCallback(): BatteryManagerGattCallback {
return HTManagerGattCallback()
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc..
*/
private inner class HTManagerGattCallback : BatteryManagerGattCallback() {
override fun initialize() {
super.initialize()
setIndicationCallback(htCharacteristic)
.with(object : TemperatureMeasurementDataCallback() {
override fun onDataReceived(device: BluetoothDevice, data: Data) {
log(
LogContract.Log.Level.APPLICATION,
"\"" + TemperatureMeasurementParser.parse(data) + "\" received"
)
super.onDataReceived(device, data)
}
override fun onTemperatureMeasurementReceived(
device: BluetoothDevice,
temperature: Float,
@TemperatureUnit unit: Int,
calendar: Calendar?,
@TemperatureType type: Int?
) {
mCallbacks!!.onTemperatureMeasurementReceived(
device,
temperature,
unit,
calendar,
type
)
}
})
enableIndications(htCharacteristic).enqueue()
}
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(HT_SERVICE_UUID)
if (service != null) {
htCharacteristic = service.getCharacteristic(HT_MEASUREMENT_CHARACTERISTIC_UUID)
}
return htCharacteristic != null
}
override fun onDeviceDisconnected() {
super.onDeviceDisconnected()
htCharacteristic = null
}
override fun onServicesInvalidated() {}
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.hts.service
import no.nordicsemi.android.ble.common.profile.ht.TemperatureMeasurementCallback
import no.nordicsemi.android.service.BatteryManagerCallbacks
/**
* Interface [HTSManagerCallbacks] must be implemented by [HTActivity] in order
* to receive callbacks from [HTSManager].
*/
interface HTSManagerCallbacks : BatteryManagerCallbacks, TemperatureMeasurementCallback

View File

@@ -0,0 +1,49 @@
package no.nordicsemi.android.hts.service
import android.bluetooth.BluetoothDevice
import dagger.hilt.android.AndroidEntryPoint
import no.nordicsemi.android.ble.BleManagerCallbacks
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.service.LoggableBleManager
import java.util.*
import javax.inject.Inject
@AndroidEntryPoint
internal class HTSService : ForegroundBleService<HTSManager>(), HTSManagerCallbacks {
private var data = HTSData()
private val points = mutableListOf<Int>()
@Inject
lateinit var localBroadcast: HTSDataBroadcast
override val manager: HTSManager by lazy {
HTSManager(this).apply {
setGattCallbacks(this@HTSService)
}
}
override fun initializeManager(): LoggableBleManager<out BleManagerCallbacks> {
return manager
}
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
sendNewData(data.copy(batteryLevel = batteryLevel))
}
override fun onTemperatureMeasurementReceived(
device: BluetoothDevice,
temperature: Float,
unit: Int,
calendar: Calendar?,
type: Int?
) {
TODO("Not yet implemented")
}
private fun sendNewData(newData: HTSData) {
data = newData
localBroadcast.offer(newData)
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.hts.service;
import java.util.Locale;
import no.nordicsemi.android.ble.data.Data;
@SuppressWarnings("ConstantConditions")
public class TemperatureMeasurementParser {
private static final byte TEMPERATURE_UNIT_FLAG = 0x01; // 1 bit
private static final byte TIMESTAMP_FLAG = 0x02; // 1 bits
private static final byte TEMPERATURE_TYPE_FLAG = 0x04; // 1 bit
public static String parse(final Data data) {
int offset = 0;
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
/*
* false Temperature is in Celsius degrees
* true Temperature is in Fahrenheit degrees
*/
final boolean fahrenheit = (flags & TEMPERATURE_UNIT_FLAG) > 0;
/*
* false No Timestamp in the packet
* true There is a timestamp information
*/
final boolean timestampIncluded = (flags & TIMESTAMP_FLAG) > 0;
/*
* false Temperature type is not included
* true Temperature type included in the packet
*/
final boolean temperatureTypeIncluded = (flags & TEMPERATURE_TYPE_FLAG) > 0;
final float tempValue = data.getFloatValue(Data.FORMAT_FLOAT, offset);
offset += 4;
String dateTime = null;
if (timestampIncluded) {
dateTime = DateTimeParser.parse(data, offset);
offset += 7;
}
String type = null;
if (temperatureTypeIncluded) {
type = TemperatureTypeParser.parse(data, offset);
// offset++;
}
final StringBuilder builder = new StringBuilder();
builder.append(String.format(Locale.US, "%.02f", tempValue));
if (fahrenheit)
builder.append("°F");
else
builder.append("°C");
if (timestampIncluded)
builder.append("\nTime: ").append(dateTime);
if (temperatureTypeIncluded)
builder.append("\nType: ").append(type);
return builder.toString();
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.hts.service
import no.nordicsemi.android.ble.data.Data
object TemperatureTypeParser {
fun parse(data: Data): String {
return parse(data, 0)
}
/* package */
@JvmStatic
fun parse(data: Data, offset: Int): String {
val type = data.value!![offset].toInt()
return when (type) {
1 -> "Armpit"
2 -> "Body (general)"
3 -> "Ear (usually ear lobe)"
4 -> "Finger"
5 -> "Gastro-intestinal Tract"
6 -> "Mouth"
7 -> "Rectum"
8 -> "Toe"
9 -> "Tympanum (ear drum)"
else -> "Unknown"
}
}
}

View File

@@ -0,0 +1,57 @@
package no.nordicsemi.android.hts.view
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.hts.R
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.theme.view.BatteryLevelView
import no.nordicsemi.android.theme.view.SensorRecordCard
@Composable
internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
SensorRecordCard {
Box(modifier = Modifier.padding(16.dp)) {
}
}
Spacer(modifier = Modifier.height(16.dp))
BatteryLevelView(state.batteryLevel)
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 Preview() {
HTSContentView(state = HTSData()) { }
}

View File

@@ -0,0 +1,52 @@
package no.nordicsemi.android.hts.view
import android.content.Intent
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
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.hts.R
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.hts.service.HTSService
import no.nordicsemi.android.hts.viewmodel.HTSViewModel
import no.nordicsemi.android.utils.isServiceRunning
@Composable
fun HTSScreen(finishAction: () -> Unit) {
val viewModel: HTSViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
val context = LocalContext.current
LaunchedEffect(state.isScreenActive) {
if (!state.isScreenActive) {
finishAction()
}
if (context.isServiceRunning(HTSService::class.java.name)) {
val intent = Intent(context, HTSService::class.java)
context.stopService(intent)
}
}
LaunchedEffect("start-service") {
if (!context.isServiceRunning(HTSService::class.java.name)) {
val intent = Intent(context, HTSService::class.java)
context.startService(intent)
}
}
HRSView(state) { viewModel.onEvent(it) }
}
@Composable
private fun HRSView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.hts_title)) })
HTSContentView(state) { onEvent(it) }
}
}

View File

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

View File

@@ -0,0 +1,47 @@
package no.nordicsemi.android.hts.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.hts.service.HTSDataBroadcast
import no.nordicsemi.android.hts.view.DisconnectEvent
import no.nordicsemi.android.hts.view.HTSScreenViewEvent
import javax.inject.Inject
@HiltViewModel
internal class HTSViewModel @Inject constructor(
private val localBroadcast: HTSDataBroadcast
) : ViewModel() {
val state = MutableStateFlow(HTSData())
init {
localBroadcast.events.onEach {
withContext(Dispatchers.Main) { consumeEvent(it) }
}.launchIn(viewModelScope)
}
private fun consumeEvent(event: HTSData) {
state.value = state.value.copy(
batteryLevel = event.batteryLevel,
sensorLocation = event.sensorLocation
)
}
fun onEvent(event: HTSScreenViewEvent) {
(event as? DisconnectEvent)?.let {
onDisconnectButtonClick()
}
}
private fun onDisconnectButtonClick() {
state.tryEmit(state.value.copy(isScreenActive = false))
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hts_title">HTS</string>
<string name="hts_celsius">%.1f °C</string>
<string name="hts_fahrenheit">%.1f °F</string>
<string name="hts_kelvin">%.1f °K</string>
</resources>

View File

@@ -0,0 +1,17 @@
package no.nordicsemi.android.hts
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

@@ -16,13 +16,11 @@ class SelectedBluetoothDeviceHolder constructor(
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
}
//TODO: Check if starts automatically
fun isDeviceBonded(): Boolean {
return device?.bondState == BluetoothDevice.BOND_NONE
}
fun bondDevice() {
device?.let {
if (it.bondState == BluetoothDevice.BOND_NONE) {
it.createBond()
}
}
device?.createBond()
}
fun forgetDevice() {

View File

@@ -0,0 +1,18 @@
package no.nordicsemi.android.theme.view
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.NordicColors
@Composable
fun SensorRecordCard(content: @Composable () -> Unit) {
Card(
backgroundColor = NordicColors.NordicGray4.value(),
shape = RoundedCornerShape(10.dp),
elevation = 0.dp
) {
content()
}
}

View File

@@ -64,6 +64,7 @@ include ':app'
include ':feature_csc'
include ':feature_gls'
include ':feature_hrs'
include ':feature_hts'
include ':feature_scanner'
include ':lib_service'