mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-01-08 09:14:23 +01:00
Add HTS module
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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"),
|
||||
|
||||
21
app/src/main/res/drawable/ic_hts.xml
Normal file
21
app/src/main/res/drawable/ic_hts.xml
Normal 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>
|
||||
@@ -55,4 +55,8 @@ internal data class CSCData(
|
||||
fun displayGearRatio(): String {
|
||||
return String.format(Locale.US, "%.1f", gearRatio)
|
||||
}
|
||||
|
||||
fun items(): List<> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) { }
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,6 +15,10 @@ internal class GLSViewModel @Inject constructor(
|
||||
val state = glsManager.data
|
||||
|
||||
fun bondDevice() {
|
||||
|
||||
if (deviceHolder.isDeviceBonded()) {
|
||||
deviceHolder.bondDevice()
|
||||
} else {
|
||||
//start work
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) { }
|
||||
}
|
||||
@@ -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
26
feature_hts/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.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)
|
||||
}
|
||||
}
|
||||
9
feature_hts/src/main/AndroidManifest.xml
Normal file
9
feature_hts/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>()
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()) { }
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.hts.view
|
||||
|
||||
sealed class HTSScreenViewEvent
|
||||
|
||||
object DisconnectEvent : HTSScreenViewEvent()
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
8
feature_hts/src/main/res/values/strings.xml
Normal file
8
feature_hts/src/main/res/values/strings.xml
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ include ':app'
|
||||
include ':feature_csc'
|
||||
include ':feature_gls'
|
||||
include ':feature_hrs'
|
||||
include ':feature_hts'
|
||||
include ':feature_scanner'
|
||||
|
||||
include ':lib_service'
|
||||
|
||||
Reference in New Issue
Block a user