Merge pull request #2562 from buessow/stage
Get heart rate from Wear watch and display as graph in Overview
This commit is contained in:
commit
0c847616cb
29 changed files with 562 additions and 17 deletions
|
@ -3,6 +3,7 @@ package info.nightscout.rx.weardata
|
|||
import info.nightscout.rx.events.Event
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.joda.time.DateTime
|
||||
import java.util.Objects
|
||||
|
||||
@Serializable
|
||||
|
@ -90,6 +91,16 @@ sealed class EventData : Event() {
|
|||
@Serializable
|
||||
data class ActionQuickWizardPreCheck(val guid: String) : EventData()
|
||||
|
||||
@Serializable
|
||||
data class ActionHeartRate(
|
||||
val duration: Long,
|
||||
val timestamp: Long,
|
||||
val beatsPerMinute: Double,
|
||||
val device: String): EventData() {
|
||||
override fun toString() =
|
||||
"HR ${beatsPerMinute.toInt()} at ${DateTime(timestamp)} for ${duration / 1000.0}sec $device"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ActionTempTargetPreCheck(
|
||||
val command: TempTargetCommand,
|
||||
|
|
|
@ -327,6 +327,7 @@ class HistoryBrowseActivity : TranslatedDaggerAppCompatActivity() {
|
|||
var useRatioForScale = false
|
||||
var useDSForScale = false
|
||||
var useBGIForScale = false
|
||||
var useHRForScale = false
|
||||
when {
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.ABS.ordinal] -> useABSForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.IOB.ordinal] -> useIobForScale = true
|
||||
|
@ -335,6 +336,7 @@ class HistoryBrowseActivity : TranslatedDaggerAppCompatActivity() {
|
|||
menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal] -> useBGIForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal] -> useRatioForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.DEVSLOPE.ordinal] -> useDSForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.HR.ordinal] -> useHRForScale = true
|
||||
}
|
||||
val alignDevBgiScale = menuChartSettings[g + 1][OverviewMenus.CharType.DEV.ordinal] && menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal]
|
||||
|
||||
|
@ -345,6 +347,7 @@ class HistoryBrowseActivity : TranslatedDaggerAppCompatActivity() {
|
|||
if (menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal]) secondGraphData.addMinusBGI(useBGIForScale, if (alignDevBgiScale) 1.0 else 0.8)
|
||||
if (menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal]) secondGraphData.addRatio(useRatioForScale, if (useRatioForScale) 1.0 else 0.8)
|
||||
if (menuChartSettings[g + 1][OverviewMenus.CharType.DEVSLOPE.ordinal] && config.isDev()) secondGraphData.addDeviationSlope(useDSForScale, 1.0)
|
||||
if (menuChartSettings[g + 1][OverviewMenus.CharType.HR.ordinal] && config.isDev()) secondGraphData.addHeartRate(useHRForScale, 1.0)
|
||||
|
||||
// set manual x bounds to have nice steps
|
||||
secondGraphData.formatAxis(historyBrowserData.overviewData.fromTime, historyBrowserData.overviewData.endTime)
|
||||
|
|
|
@ -150,4 +150,7 @@ interface OverviewData {
|
|||
val dsMinScale: Scale
|
||||
var dsMaxSeries: LineGraphSeries<ScaledDataPoint>
|
||||
var dsMinSeries: LineGraphSeries<ScaledDataPoint>
|
||||
}
|
||||
var heartRateScale: Scale
|
||||
var heartRateGraphSeries: LineGraphSeries<DataPointWithLabelInterface>
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package info.nightscout.core.graph.data
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import info.nightscout.database.entities.HeartRate
|
||||
import info.nightscout.shared.interfaces.ResourceHelper
|
||||
|
||||
class HeartRateDataPoint(
|
||||
private val data: HeartRate,
|
||||
private val rh: ResourceHelper,
|
||||
) : DataPointWithLabelInterface {
|
||||
|
||||
override fun getX(): Double = (data.timestamp - data.duration).toDouble()
|
||||
override fun getY(): Double = data.beatsPerMinute
|
||||
override fun setY(y: Double) {}
|
||||
|
||||
override val label: String = ""
|
||||
override val duration = data.duration
|
||||
override val shape = PointsWithLabelGraphSeries.Shape.HEARTRATE
|
||||
override val size = 1f
|
||||
override val paintStyle: Paint.Style = Paint.Style.FILL
|
||||
|
||||
override fun color(context: Context?): Int = rh.gac(context, info.nightscout.core.ui.R.attr.heartRateColor)
|
||||
}
|
|
@ -54,7 +54,8 @@ public class PointsWithLabelGraphSeries<E extends DataPointWithLabelInterface> e
|
|||
GENERAL_WITH_DURATION,
|
||||
COB_FAIL_OVER,
|
||||
IOB_PREDICTION,
|
||||
BUCKETED_BG
|
||||
BUCKETED_BG,
|
||||
HEARTRATE,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -324,6 +325,10 @@ public class PointsWithLabelGraphSeries<E extends DataPointWithLabelInterface> e
|
|||
mPaint.setStrokeWidth(5);
|
||||
canvas.drawRect(endX - 3, bounds.top + py - 3, xPlusLength + 3, bounds.bottom + py + 3, mPaint);
|
||||
}
|
||||
} else if (value.getShape() == Shape.HEARTRATE) {
|
||||
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
mPaint.setStrokeWidth(0);
|
||||
canvas.drawCircle(endX, endY, 1F, mPaint);
|
||||
}
|
||||
// set values above point
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@ interface OverviewMenus {
|
|||
BGI,
|
||||
SEN,
|
||||
ACT,
|
||||
DEVSLOPE
|
||||
DEVSLOPE,
|
||||
HR,
|
||||
}
|
||||
|
||||
val setting: List<Array<Boolean>>
|
||||
|
@ -23,4 +24,4 @@ interface OverviewMenus {
|
|||
fun setupChartMenu(context: Context, chartButton: ImageButton)
|
||||
fun enabledTypes(graph: Int): String
|
||||
fun isEnabledIn(type: CharType): Int
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,6 +216,7 @@
|
|||
<item name="inRangeBackground">@color/inRangeBackground</item>
|
||||
<item name="devSlopePosColor">@color/devSlopePos</item>
|
||||
<item name="devSlopeNegColor">@color/devSlopeNeg</item>
|
||||
<item name="heartRateColor">@color/heartRate</item>
|
||||
<item name="deviationGreyColor">@color/deviationGrey</item>
|
||||
<item name="deviationBlackColor">@color/deviationBlack</item>
|
||||
<item name="deviationGreenColor">@color/deviationGreen</item>
|
||||
|
|
|
@ -181,6 +181,7 @@
|
|||
<attr name="bolusDataPointColor" format="reference|color" />
|
||||
<attr name="profileSwitchColor" format="reference|color" />
|
||||
<attr name="originalBgValueColor" format="reference|color" />
|
||||
<attr name="heartRateColor" format="reference|color" />
|
||||
<attr name="therapyEvent_NS_MBG" format="reference|color" />
|
||||
<attr name="therapyEvent_FINGER_STICK_BG_VALUE" format="reference|color" />
|
||||
<attr name="therapyEvent_EXERCISE" format="reference|color" />
|
||||
|
@ -221,4 +222,4 @@
|
|||
<attr name="crossTargetColor" format="reference|color" />
|
||||
<!---Custom button -->
|
||||
<attr name="customBtnStyle" format="reference"/>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -153,6 +153,7 @@
|
|||
<color name="bgi">#00EEEE</color>
|
||||
<color name="devSlopePos">#FFFFFF00</color>
|
||||
<color name="devSlopeNeg">#FFFF00FF</color>
|
||||
<color name="heartRate">#FFFFFF66</color>
|
||||
<color name="actionsConfirm">#F6CE22</color>
|
||||
<color name="deviations">#FF0000</color>
|
||||
<color name="cobAlert">#7484E2</color>
|
||||
|
|
|
@ -219,6 +219,7 @@
|
|||
<item name="inRangeBackground">@color/inRangeBackground</item>
|
||||
<item name="devSlopePosColor">@color/devSlopePos</item>
|
||||
<item name="devSlopeNegColor">@color/devSlopeNeg</item>
|
||||
<item name="heartRateColor">@color/heartRate</item>
|
||||
<item name="deviationGreyColor">@color/deviationGrey</item>
|
||||
<item name="deviationBlackColor">@color/deviationBlack</item>
|
||||
<item name="deviationGreenColor">@color/deviationGreen</item>
|
||||
|
|
|
@ -92,8 +92,26 @@ internal class HeartRateDaoTest {
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Test
|
||||
fun getFromTimeToTime() {
|
||||
createDatabase().use { db ->
|
||||
val dao = db.heartRateDao
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val hr1 = createHeartRate(timestamp = timestamp, beatsPerMinute = 80.0)
|
||||
val hr2 = createHeartRate(timestamp = timestamp + 1, beatsPerMinute = 150.0)
|
||||
val hr3 = createHeartRate(timestamp = timestamp + 2, beatsPerMinute = 160.0)
|
||||
dao.insertNewEntry(hr1)
|
||||
dao.insertNewEntry(hr2)
|
||||
dao.insertNewEntry(hr3)
|
||||
|
||||
assertEquals(listOf(hr1, hr2, hr3), dao.getFromTimeToTime(timestamp, timestamp + 2))
|
||||
assertEquals(listOf(hr1, hr2), dao.getFromTimeToTime(timestamp, timestamp + 1))
|
||||
assertEquals(listOf(hr2), dao.getFromTimeToTime(timestamp + 1, timestamp + 1))
|
||||
assertTrue(dao.getFromTimeToTime(timestamp + 3, timestamp + 10).isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TEST_DB_NAME = "testDatabase"
|
||||
|
||||
fun createHeartRate(timestamp: Long? = null, beatsPerMinute: Double = 80.0) =
|
||||
|
|
|
@ -934,6 +934,9 @@ import kotlin.math.roundToInt
|
|||
|
||||
fun getHeartRatesFromTime(timeMillis: Long) = database.heartRateDao.getFromTime(timeMillis)
|
||||
|
||||
fun getHeartRatesFromTimeToTime(startMillis: Long, endMillis: Long) =
|
||||
database.heartRateDao.getFromTimeToTime(startMillis, endMillis)
|
||||
|
||||
suspend fun collectNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int) = NewEntries(
|
||||
apsResults = database.apsResultDao.getNewEntriesSince(since, until, limit, offset),
|
||||
apsResultLinks = database.apsResultLinkDao.getNewEntriesSince(since, until, limit, offset),
|
||||
|
|
|
@ -23,6 +23,9 @@ internal interface HeartRateDao : TraceableDao<HeartRate> {
|
|||
@Query("SELECT * FROM $TABLE_HEART_RATE WHERE timestamp >= :timestamp ORDER BY timestamp")
|
||||
fun getFromTime(timestamp: Long): List<HeartRate>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_HEART_RATE WHERE timestamp BETWEEN :startMillis AND :endMillis ORDER BY timestamp")
|
||||
fun getFromTimeToTime(startMillis: Long, endMillis: Long): List<HeartRate>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_HEART_RATE WHERE timestamp > :since AND timestamp <= :until LIMIT :limit OFFSET :offset")
|
||||
fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<HeartRate>
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ class OverviewDataImpl @Inject constructor(
|
|||
dsMinSeries = LineGraphSeries()
|
||||
treatmentsSeries = PointsWithLabelGraphSeries()
|
||||
epsSeries = PointsWithLabelGraphSeries()
|
||||
heartRateGraphSeries = LineGraphSeries()
|
||||
}
|
||||
|
||||
override fun initRange() {
|
||||
|
@ -322,4 +323,6 @@ class OverviewDataImpl @Inject constructor(
|
|||
override val dsMinScale = Scale()
|
||||
override var dsMaxSeries: LineGraphSeries<ScaledDataPoint> = LineGraphSeries()
|
||||
override var dsMinSeries: LineGraphSeries<ScaledDataPoint> = LineGraphSeries()
|
||||
override var heartRateScale = Scale()
|
||||
override var heartRateGraphSeries: LineGraphSeries<DataPointWithLabelInterface> = LineGraphSeries()
|
||||
}
|
||||
|
|
|
@ -1030,6 +1030,7 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList
|
|||
var useRatioForScale = false
|
||||
var useDSForScale = false
|
||||
var useBGIForScale = false
|
||||
var useHRForScale = false
|
||||
when {
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.ABS.ordinal] -> useABSForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.IOB.ordinal] -> useIobForScale = true
|
||||
|
@ -1038,6 +1039,7 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList
|
|||
menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal] -> useBGIForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal] -> useRatioForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.DEVSLOPE.ordinal] -> useDSForScale = true
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.HR.ordinal] -> useHRForScale = true
|
||||
}
|
||||
val alignDevBgiScale = menuChartSettings[g + 1][OverviewMenus.CharType.DEV.ordinal] && menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal]
|
||||
|
||||
|
@ -1052,6 +1054,7 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList
|
|||
if (useDSForScale) 1.0 else 0.8,
|
||||
useRatioForScale
|
||||
)
|
||||
if (menuChartSettings[g + 1][OverviewMenus.CharType.HR.ordinal]) secondGraphData.addHeartRate(useHRForScale, if (useHRForScale) 1.0 else 0.8)
|
||||
|
||||
// set manual x bounds to have nice steps
|
||||
secondGraphData.formatAxis(overviewData.fromTime, overviewData.endTime)
|
||||
|
@ -1067,7 +1070,8 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList
|
|||
menuChartSettings[g + 1][OverviewMenus.CharType.DEV.ordinal] ||
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal] ||
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal] ||
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.DEVSLOPE.ordinal]
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.DEVSLOPE.ordinal] ||
|
||||
menuChartSettings[g + 1][OverviewMenus.CharType.HR.ordinal]
|
||||
).toVisibility()
|
||||
secondaryGraphsData[g].performUpdate()
|
||||
}
|
||||
|
|
|
@ -47,7 +47,8 @@ class OverviewMenusImpl @Inject constructor(
|
|||
BGI(R.string.overview_show_bgi, info.nightscout.core.ui.R.attr.bgiColor, info.nightscout.core.ui.R.attr.menuTextColor, primary = false, secondary = true, shortnameId = R.string.bgi_shortname),
|
||||
SEN(R.string.overview_show_sensitivity, info.nightscout.core.ui.R.attr.ratioColor, info.nightscout.core.ui.R.attr.menuTextColorInverse, primary = false, secondary = true, shortnameId = R.string.sensitivity_shortname),
|
||||
ACT(R.string.overview_show_activity, info.nightscout.core.ui.R.attr.activityColor, info.nightscout.core.ui.R.attr.menuTextColor, primary = true, secondary = false, shortnameId = R.string.activity_shortname),
|
||||
DEVSLOPE(R.string.overview_show_deviation_slope, info.nightscout.core.ui.R.attr.devSlopePosColor, info.nightscout.core.ui.R.attr.menuTextColor, primary = false, secondary = true, shortnameId = R.string.devslope_shortname)
|
||||
DEVSLOPE(R.string.overview_show_deviation_slope, info.nightscout.core.ui.R.attr.devSlopePosColor, info.nightscout.core.ui.R.attr.menuTextColor, primary = false, secondary = true, shortnameId = R.string.devslope_shortname),
|
||||
HR(R.string.overview_show_heartRate, info.nightscout.core.ui.R.attr.heartRateColor, info.nightscout.core.ui.R.attr.menuTextColor, primary = false, secondary = true, shortnameId = R.string.heartRate_shortname),
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -202,4 +203,4 @@ class OverviewMenusImpl @Inject constructor(
|
|||
return -1
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,5 +258,14 @@ class GraphData(
|
|||
// draw it
|
||||
graph.onDataChanged(false, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun addHeartRate(useForScale: Boolean, scale: Double) {
|
||||
val maxHR = overviewData.heartRateGraphSeries.highestValueY
|
||||
if (useForScale) {
|
||||
minY = 0.0
|
||||
maxY = maxHR
|
||||
}
|
||||
addSeries(overviewData.heartRateGraphSeries)
|
||||
overviewData.heartRateScale.multiplier = maxY * scale / maxHR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import info.nightscout.database.ValueWrapper
|
|||
import info.nightscout.database.entities.Bolus
|
||||
import info.nightscout.database.entities.BolusCalculatorResult
|
||||
import info.nightscout.database.entities.GlucoseValue
|
||||
import info.nightscout.database.entities.HeartRate
|
||||
import info.nightscout.database.entities.TemporaryBasal
|
||||
import info.nightscout.database.entities.TemporaryTarget
|
||||
import info.nightscout.database.entities.TotalDailyDose
|
||||
|
@ -28,6 +29,7 @@ import info.nightscout.database.entities.interfaces.end
|
|||
import info.nightscout.database.impl.AppRepository
|
||||
import info.nightscout.database.impl.transactions.CancelCurrentTemporaryTargetIfAnyTransaction
|
||||
import info.nightscout.database.impl.transactions.InsertAndCancelCurrentTemporaryTargetTransaction
|
||||
import info.nightscout.database.impl.transactions.InsertOrUpdateHeartRateTransaction
|
||||
import info.nightscout.interfaces.Config
|
||||
import info.nightscout.interfaces.Constants
|
||||
import info.nightscout.interfaces.GlucoseUnit
|
||||
|
@ -308,6 +310,10 @@ class DataHandlerMobile @Inject constructor(
|
|||
aapsLogger.debug(LTag.WEAR, "WearException received $it from ${it.sourceNodeId}")
|
||||
fabricPrivacy.logWearException(it)
|
||||
}, fabricPrivacy::logException)
|
||||
disposable += rxBus
|
||||
.toObservable(EventData.ActionHeartRate::class.java)
|
||||
.observeOn(aapsSchedulers.io)
|
||||
.subscribe({ handleHeartRate(it) }, fabricPrivacy::logException)
|
||||
}
|
||||
|
||||
private fun handleTddStatus() {
|
||||
|
@ -1230,4 +1236,15 @@ class DataHandlerMobile @Inject constructor(
|
|||
@Synchronized private fun sendError(errorMessage: String) {
|
||||
rxBus.send(EventMobileToWear(EventData.ConfirmAction(rh.gs(info.nightscout.core.ui.R.string.error), errorMessage, returnCommand = EventData.Error(dateUtil.now())))) // ignore return path
|
||||
}
|
||||
|
||||
/** Stores heart rate events coming from the Wear device. */
|
||||
private fun handleHeartRate(actionHeartRate: EventData.ActionHeartRate) {
|
||||
aapsLogger.debug(LTag.WEAR, "Heart rate received $actionHeartRate from ${actionHeartRate.sourceNodeId}")
|
||||
val hr = HeartRate(
|
||||
duration = actionHeartRate.duration,
|
||||
timestamp = actionHeartRate.timestamp,
|
||||
beatsPerMinute = actionHeartRate.beatsPerMinute,
|
||||
device = actionHeartRate.device)
|
||||
repository.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -291,6 +291,7 @@
|
|||
|
||||
<string name="overview_show_predictions">Predictions</string>
|
||||
<string name="overview_show_treatments">Treatments</string>
|
||||
<string name="overview_show_heartRate">Heart Rate</string>
|
||||
<string name="overview_show_deviation_slope">Deviation slope</string>
|
||||
<string name="overview_show_activity">Activity</string>
|
||||
<string name="overview_show_bgi">Blood Glucose Impact</string>
|
||||
|
@ -308,6 +309,7 @@
|
|||
<string name="abs_insulin_shortname">ABS</string>
|
||||
<string name="devslope_shortname">DEVSLOPE</string>
|
||||
<string name="treatments_shortname">TREAT</string>
|
||||
<string name="heartRate_shortname">HR</string>
|
||||
<string name="sensitivity_shortname">SENS</string>
|
||||
<string name="graph_scale">Graph scale</string>
|
||||
<string name="graph_menu_divider_header">Graph</string>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.BODY_SENSORS" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.wear.tiles" />
|
||||
|
||||
|
@ -268,6 +269,10 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".heartrate.HeartRateListener"
|
||||
android:exported="true" />
|
||||
|
||||
<service
|
||||
android:name=".complications.LongStatusComplication"
|
||||
android:icon="@drawable/ic_aaps_full"
|
||||
|
|
|
@ -4,6 +4,7 @@ import dagger.Module
|
|||
import dagger.android.ContributesAndroidInjector
|
||||
import info.nightscout.androidaps.comm.DataLayerListenerServiceWear
|
||||
import info.nightscout.androidaps.complications.*
|
||||
import info.nightscout.androidaps.heartrate.HeartRateListener
|
||||
import info.nightscout.androidaps.tile.*
|
||||
import info.nightscout.androidaps.watchfaces.*
|
||||
import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace
|
||||
|
@ -13,7 +14,7 @@ import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace
|
|||
abstract class WearServicesModule {
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesDataLayerListenerService(): DataLayerListenerServiceWear
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesHeartRateListenerService(): HeartRateListener
|
||||
@ContributesAndroidInjector abstract fun contributesBaseComplicationProviderService(): BaseComplicationProviderService
|
||||
@ContributesAndroidInjector abstract fun contributesBrCobIobComplication(): BrCobIobComplication
|
||||
@ContributesAndroidInjector abstract fun contributesCobDetailedComplication(): CobDetailedComplication
|
||||
|
@ -45,4 +46,4 @@ abstract class WearServicesModule {
|
|||
@ContributesAndroidInjector abstract fun contributesTempTargetTileService(): TempTargetTileService
|
||||
@ContributesAndroidInjector abstract fun contributesActionsTileService(): ActionsTileService
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
package info.nightscout.androidaps.heartrate
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.SENSOR_SERVICE
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import info.nightscout.androidaps.comm.IntentWearToMobile
|
||||
import info.nightscout.rx.AapsSchedulers
|
||||
import info.nightscout.rx.logging.AAPSLogger
|
||||
import info.nightscout.rx.logging.LTag
|
||||
import info.nightscout.rx.weardata.EventData
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Gets heart rate readings from watch and sends them to the phone.
|
||||
*
|
||||
* The Android API doesn't define how often heart rate events are sent do the
|
||||
* listener, it could be once per second or only when the heart rate changes.
|
||||
*
|
||||
* Heart rate is not a point in time measurement but is always sampled over a
|
||||
* certain time, i.e. you count the the number of heart beats and divide by the
|
||||
* minutes that have passed. Therefore, the provided value has to be for the past.
|
||||
* However, we ignore this here.
|
||||
*
|
||||
* We don't need very exact values, but rather values that are easy to consume
|
||||
* and don't produce too much data that would cause much battery consumption.
|
||||
* Therefore, this class averages the heart rate over a minute ([samplingIntervalMillis])
|
||||
* and sends this value to the phone.
|
||||
*
|
||||
* We will not always get valid values, e.g. if the watch is taken of. The listener
|
||||
* ignores such time unless we don't get good values for more than 90% of time. Since
|
||||
* heart rate doesn't change so fast this should be good enough.
|
||||
*/
|
||||
class HeartRateListener(
|
||||
private val ctx: Context,
|
||||
private val aapsLogger: AAPSLogger,
|
||||
aapsSchedulers: AapsSchedulers,
|
||||
now: Long = System.currentTimeMillis(),
|
||||
) : SensorEventListener, Disposable {
|
||||
|
||||
/** How often we send values to the phone. */
|
||||
private val samplingIntervalMillis = 60_000L
|
||||
private val sampler = Sampler(now)
|
||||
private var schedule: Disposable? = null
|
||||
|
||||
/** We only use values with these accuracies and ignore NO_CONTACT and UNRELIABLE. */
|
||||
private val goodAccuracies = arrayOf(
|
||||
SensorManager.SENSOR_STATUS_ACCURACY_LOW,
|
||||
SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM,
|
||||
SensorManager.SENSOR_STATUS_ACCURACY_HIGH,
|
||||
)
|
||||
|
||||
init {
|
||||
aapsLogger.info(LTag.WEAR, "Create ${javaClass.simpleName}")
|
||||
val sensorManager = ctx.getSystemService(SENSOR_SERVICE) as SensorManager?
|
||||
if (sensorManager == null) {
|
||||
aapsLogger.warn(LTag.WEAR, "Cannot get sensor manager to get heart rate readings")
|
||||
} else {
|
||||
val heartRateSensor = sensorManager.getDefaultSensor(Sensor.TYPE_HEART_RATE)
|
||||
if (heartRateSensor == null) {
|
||||
aapsLogger.warn(LTag.WEAR, "Cannot get heart rate sensor")
|
||||
} else {
|
||||
sensorManager.registerListener(this, heartRateSensor, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
}
|
||||
}
|
||||
schedule = aapsSchedulers.io.schedulePeriodicallyDirect(
|
||||
::send, samplingIntervalMillis, samplingIntervalMillis, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most recent heart rate reading and null if there is no valid
|
||||
* value at the moment.
|
||||
*/
|
||||
val currentHeartRateBpm get() = sampler.currentBpm?.roundToInt()
|
||||
|
||||
@VisibleForTesting
|
||||
var sendHeartRate: (EventData.ActionHeartRate)->Unit = { hr -> ctx.startService(IntentWearToMobile(ctx, hr)) }
|
||||
|
||||
override fun isDisposed() = schedule == null
|
||||
|
||||
override fun dispose() {
|
||||
aapsLogger.info(LTag.WEAR, "Dispose ${javaClass.simpleName}")
|
||||
schedule?.dispose()
|
||||
(ctx.getSystemService(SENSOR_SERVICE) as SensorManager?)?.unregisterListener(this)
|
||||
}
|
||||
|
||||
/** Sends currently sampled value to the phone. Executed every [samplingIntervalMillis]. */
|
||||
private fun send() {
|
||||
send(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun send(timestampMillis: Long) {
|
||||
sampler.getAndReset(timestampMillis)?.let { hr ->
|
||||
aapsLogger.info(LTag.WEAR, "Send heart rate $hr")
|
||||
sendHeartRate(hr)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||
onAccuracyChanged(sensor.type, accuracy, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun onAccuracyChanged(sensorType: Int, accuracy: Int, timestampMillis: Long) {
|
||||
if (sensorType != Sensor.TYPE_HEART_RATE) {
|
||||
aapsLogger.error(LTag.WEAR, "Invalid SensorEvent $sensorType $accuracy")
|
||||
return
|
||||
}
|
||||
if (accuracy !in goodAccuracies) sampler.setHeartRate(timestampMillis, null)
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
onSensorChanged(event.sensor?.type, event.accuracy, System.currentTimeMillis(), event.values)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun onSensorChanged(sensorType: Int?, accuracy: Int, timestampMillis: Long, values: FloatArray) {
|
||||
if (sensorType == null || sensorType != Sensor.TYPE_HEART_RATE || values.isEmpty()) {
|
||||
aapsLogger.error(LTag.WEAR, "Invalid SensorEvent $sensorType $accuracy $timestampMillis ${values.joinToString()}")
|
||||
return
|
||||
}
|
||||
val heartRate = values[0].toDouble().takeIf { accuracy in goodAccuracies }
|
||||
sampler.setHeartRate(timestampMillis, heartRate)
|
||||
}
|
||||
|
||||
private class Sampler(timestampMillis: Long) {
|
||||
private var startMillis: Long = timestampMillis
|
||||
private var lastEventMillis: Long = timestampMillis
|
||||
/** Number of heart beats sampled so far. */
|
||||
private var beats: Double = 0.0
|
||||
/** Time we could sample valid values during the current sampling interval. */
|
||||
private var activeMillis: Long = 0
|
||||
private val device = (Build.MANUFACTURER ?: "unknown") + " " + (Build.MODEL ?: "unknown")
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
var currentBpm: Double? = null
|
||||
get() = field
|
||||
private set(value) { field = value }
|
||||
|
||||
private fun Long.toMinute(): Double = this / 60_000.0
|
||||
|
||||
private fun fix(timestampMillis: Long) {
|
||||
currentBpm?.let { bpm ->
|
||||
val elapsed = timestampMillis - lastEventMillis
|
||||
beats += elapsed.toMinute() * bpm
|
||||
activeMillis += elapsed
|
||||
}
|
||||
lastEventMillis = timestampMillis
|
||||
}
|
||||
|
||||
/** Gets the current sampled value and resets the samplers clock to the given timestamp. */
|
||||
fun getAndReset(timestampMillis: Long): EventData.ActionHeartRate? {
|
||||
lock.withLock {
|
||||
fix(timestampMillis)
|
||||
return if (10 * activeMillis > lastEventMillis - startMillis) {
|
||||
val bpm = beats / activeMillis.toMinute()
|
||||
EventData.ActionHeartRate(timestampMillis - startMillis, timestampMillis, bpm, device)
|
||||
} else {
|
||||
null
|
||||
}.also {
|
||||
startMillis = timestampMillis
|
||||
lastEventMillis = timestampMillis
|
||||
beats = 0.0
|
||||
activeMillis = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setHeartRate(timestampMillis: Long, heartRate: Double?) {
|
||||
lock.withLock {
|
||||
if (timestampMillis < lastEventMillis) return
|
||||
fix(timestampMillis)
|
||||
currentBpm = heartRate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package info.nightscout.androidaps.interaction
|
||||
|
||||
import preference.WearPreferenceActivity
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -9,7 +8,7 @@ import dagger.android.AndroidInjection
|
|||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.rx.logging.AAPSLogger
|
||||
import info.nightscout.rx.logging.LTag
|
||||
|
||||
import preference.WearPreferenceActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConfigurationActivity : WearPreferenceActivity() {
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
package info.nightscout.androidaps.interaction
|
||||
|
||||
import android.Manifest
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.rx.logging.AAPSLogger
|
||||
import info.nightscout.rx.logging.LTag
|
||||
import preference.WearPreferenceActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class WatchfaceConfigurationActivity : WearPreferenceActivity() {
|
||||
class WatchfaceConfigurationActivity : WearPreferenceActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
@Inject lateinit var aapsLogger: AAPSLogger
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
|
||||
val view = window.decorView as ViewGroup
|
||||
removeBackgroundRecursively(view)
|
||||
view.background = ContextCompat.getDrawable(this, R.drawable.settings_background)
|
||||
|
@ -24,4 +35,38 @@ class WatchfaceConfigurationActivity : WearPreferenceActivity() {
|
|||
removeBackgroundRecursively(parent.getChildAt(i))
|
||||
parent.background = null
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sp: SharedPreferences, key: String?) {
|
||||
if (key == getString(R.string.key_heart_rate_sampling)) {
|
||||
if (sp.getBoolean(key, false)) {
|
||||
requestBodySensorPermission()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestBodySensorPermission() {
|
||||
val permission = Manifest.permission.BODY_SENSORS
|
||||
if (ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(permission), BODY_SENSOR_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
if (requestCode == BODY_SENSOR_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
aapsLogger.info(LTag.WEAR, "Sensor permission for heart rate granted")
|
||||
} else {
|
||||
aapsLogger.warn(LTag.WEAR, "Sensor permission for heart rate denied")
|
||||
}
|
||||
} else {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BODY_SENSOR_PERMISSION_REQUEST_CODE = 1
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import dagger.android.AndroidInjection
|
|||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.data.RawDisplayData
|
||||
import info.nightscout.androidaps.events.EventWearPreferenceChange
|
||||
import info.nightscout.androidaps.heartrate.HeartRateListener
|
||||
import info.nightscout.androidaps.interaction.menus.MainMenuActivity
|
||||
import info.nightscout.androidaps.interaction.utils.Persistence
|
||||
import info.nightscout.androidaps.interaction.utils.WearUtil
|
||||
|
@ -99,6 +100,7 @@ abstract class BaseWatchFace : WatchFace() {
|
|||
|
||||
private var mLastSvg = ""
|
||||
private var mLastDirection = ""
|
||||
private var heartRateListener: HeartRateListener? = null
|
||||
|
||||
override fun onCreate() {
|
||||
// Not derived from DaggerService, do injection here
|
||||
|
@ -115,6 +117,7 @@ abstract class BaseWatchFace : WatchFace() {
|
|||
.subscribe { event: EventWearPreferenceChange ->
|
||||
simpleUi.updatePreferences()
|
||||
if (event.changedKey != null && event.changedKey == "delta_granularity") rxBus.send(EventWearToMobile(ActionResendData("BaseWatchFace:onSharedPreferenceChanged")))
|
||||
if (event.changedKey == getString(R.string.key_heart_rate_sampling)) updateHeartRateListener()
|
||||
if (layoutSet) setDataFields()
|
||||
invalidate()
|
||||
}
|
||||
|
@ -139,6 +142,7 @@ abstract class BaseWatchFace : WatchFace() {
|
|||
layoutView = binding.root
|
||||
performViewSetup()
|
||||
rxBus.send(EventWearToMobile(ActionResendData("BaseWatchFace::onCreate")))
|
||||
updateHeartRateListener()
|
||||
}
|
||||
|
||||
private fun forceUpdate() {
|
||||
|
@ -146,6 +150,20 @@ abstract class BaseWatchFace : WatchFace() {
|
|||
invalidate()
|
||||
}
|
||||
|
||||
private fun updateHeartRateListener() {
|
||||
if (sp.getBoolean(R.string.key_heart_rate_sampling, false)) {
|
||||
if (heartRateListener == null) {
|
||||
heartRateListener = HeartRateListener(
|
||||
this, aapsLogger, aapsSchedulers).also { hrl -> disposable += hrl }
|
||||
}
|
||||
} else {
|
||||
heartRateListener?.let { hrl ->
|
||||
disposable.remove(hrl)
|
||||
heartRateListener = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTapCommand(tapType: Int, x: Int, y: Int, eventTime: Long) {
|
||||
binding.chart?.let { chart ->
|
||||
if (tapType == TAP_TYPE_TAP && x >= chart.left && x <= chart.right && y >= chart.top && y <= chart.bottom) {
|
||||
|
|
|
@ -233,5 +233,6 @@
|
|||
<string name="old">old</string>
|
||||
<string name="old_warning">!old!</string>
|
||||
<string name="error">!err!</string>
|
||||
|
||||
<string name="key_heart_rate_sampling" translatable="false">heart_rate_sampling</string>
|
||||
<string name="pref_heartRateSampling">Heart Rate</string>
|
||||
</resources>
|
||||
|
|
|
@ -180,4 +180,11 @@
|
|||
android:summary="Input Design"
|
||||
android:title="@string/pref_moreWatchfaceSettings" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/key_heart_rate_sampling"
|
||||
android:summary="Enable heart rate sampling."
|
||||
android:title="@string/pref_heartRateSampling"
|
||||
app:wear_iconOff="@drawable/settings_off"
|
||||
app:wear_iconOn="@drawable/settings_on" />
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
package info.nightscout.androidaps.heartrate
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import info.nightscout.rx.AapsSchedulers
|
||||
import info.nightscout.rx.logging.AAPSLoggerTest
|
||||
import info.nightscout.rx.weardata.EventData.ActionHeartRate
|
||||
import io.reactivex.rxjava3.core.Scheduler
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal class HeartRateListenerTest {
|
||||
private val aapsLogger = AAPSLoggerTest()
|
||||
private val aapsSchedulers = object: AapsSchedulers {
|
||||
override val main: Scheduler = mock(Scheduler::class.java)
|
||||
override val io: Scheduler = mock(Scheduler::class.java)
|
||||
override val cpu: Scheduler = mock(Scheduler::class.java)
|
||||
override val newThread: Scheduler = mock(Scheduler::class.java)
|
||||
}
|
||||
private val schedule = mock(Disposable::class.java)
|
||||
private val heartRates = mutableListOf<ActionHeartRate>()
|
||||
private val device = "unknown unknown"
|
||||
|
||||
private fun create(timestampMillis: Long): HeartRateListener {
|
||||
val ctx = mock(Context::class.java)
|
||||
`when`(aapsSchedulers.io.schedulePeriodicallyDirect(
|
||||
any(), eq(60_000L), eq(60_000L), eq(TimeUnit.MILLISECONDS))).thenReturn(schedule)
|
||||
val listener = HeartRateListener(ctx, aapsLogger, aapsSchedulers, timestampMillis)
|
||||
verify(aapsSchedulers.io).schedulePeriodicallyDirect(
|
||||
any(), eq(60_000L), eq(60_000L), eq(TimeUnit.MILLISECONDS))
|
||||
listener.sendHeartRate = { hr -> heartRates.add(hr) }
|
||||
return listener
|
||||
}
|
||||
|
||||
private fun sendSensorEvent(
|
||||
listener: HeartRateListener,
|
||||
timestamp: Long,
|
||||
heartRate: Int,
|
||||
sensorType: Int? = Sensor.TYPE_HEART_RATE,
|
||||
accuracy: Int = SensorManager.SENSOR_STATUS_ACCURACY_HIGH) {
|
||||
listener.onSensorChanged(sensorType, accuracy, timestamp, floatArrayOf(heartRate.toFloat()))
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun before() {
|
||||
heartRates.clear()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun cleanup() {
|
||||
Mockito.verifyNoInteractions(aapsSchedulers.main)
|
||||
Mockito.verifyNoMoreInteractions(aapsSchedulers.io)
|
||||
Mockito.verifyNoInteractions(aapsSchedulers.cpu)
|
||||
Mockito.verifyNoInteractions(aapsSchedulers.newThread)
|
||||
verify(schedule).dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSensorChanged() {
|
||||
val start = System.currentTimeMillis()
|
||||
val d1 = 10_000L
|
||||
val d2 = 20_000L
|
||||
val listener = create(start)
|
||||
|
||||
assertNull(listener.currentHeartRateBpm)
|
||||
sendSensorEvent(listener, start + d1, 80)
|
||||
assertEquals(0, heartRates.size)
|
||||
assertEquals(80, listener.currentHeartRateBpm)
|
||||
|
||||
listener.send(start + d2)
|
||||
assertEquals(1, heartRates.size)
|
||||
assertEquals(ActionHeartRate(d2, start + d2, 80.0, device), heartRates.first())
|
||||
listener.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSensorChanged2() {
|
||||
val start = System.currentTimeMillis()
|
||||
val d1 = 10_000L
|
||||
val d2 = 40_000L
|
||||
val listener = create(start)
|
||||
|
||||
sendSensorEvent(listener, start, 80)
|
||||
assertEquals(0, heartRates.size)
|
||||
assertEquals(80, listener.currentHeartRateBpm)
|
||||
sendSensorEvent(listener, start + d1,100)
|
||||
assertEquals(0, heartRates.size)
|
||||
assertEquals(100, listener.currentHeartRateBpm)
|
||||
|
||||
|
||||
listener.send(start + d2)
|
||||
assertEquals(1, heartRates.size)
|
||||
assertEquals(ActionHeartRate(d2, start + d2, 95.0, device), heartRates.first())
|
||||
listener.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSensorChangedMultiple() {
|
||||
val start = System.currentTimeMillis()
|
||||
val d1 = 10_000L
|
||||
val d2 = 40_000L
|
||||
val listener = create(start)
|
||||
|
||||
sendSensorEvent(listener, start, 80)
|
||||
listener.send(start + d1)
|
||||
assertEquals(1, heartRates.size)
|
||||
|
||||
sendSensorEvent(listener, start + d1,100)
|
||||
assertEquals(1, heartRates.size)
|
||||
listener.send(start + d2)
|
||||
assertEquals(2, heartRates.size)
|
||||
|
||||
assertEquals(ActionHeartRate(d1, start + d1, 80.0, device), heartRates[0])
|
||||
assertEquals(ActionHeartRate(d2 - d1, start + d2, 100.0, device), heartRates[1])
|
||||
listener.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSensorChangedNoContact() {
|
||||
val start = System.currentTimeMillis()
|
||||
val d1 = 10_000L
|
||||
val d2 = 40_000L
|
||||
val listener = create(start)
|
||||
|
||||
sendSensorEvent(listener, start, 80)
|
||||
sendSensorEvent(listener, start + d1, 100, accuracy = SensorManager.SENSOR_STATUS_NO_CONTACT)
|
||||
assertNull(listener.currentHeartRateBpm)
|
||||
listener.send(start + d2)
|
||||
|
||||
assertEquals(1, heartRates.size)
|
||||
assertEquals(ActionHeartRate(d2, start + d2, 80.0, device), heartRates.first())
|
||||
listener.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAccuracyChanged() {
|
||||
val start = System.currentTimeMillis()
|
||||
val d1 = 10_000L
|
||||
val d2 = 40_000L
|
||||
val d3 = 70_000L
|
||||
val listener = create(start)
|
||||
|
||||
sendSensorEvent(listener, start, 80)
|
||||
listener.onAccuracyChanged(Sensor.TYPE_HEART_RATE, SensorManager.SENSOR_STATUS_UNRELIABLE, start + d1)
|
||||
sendSensorEvent(listener, start + d2, 100)
|
||||
listener.send(start + d3)
|
||||
|
||||
assertEquals(1, heartRates.size)
|
||||
assertEquals(ActionHeartRate(d3, start + d3, 95.0, device), heartRates.first())
|
||||
listener.dispose()
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package info.nightscout.workflow
|
|||
import android.content.Context
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.jjoe64.graphview.series.LineGraphSeries
|
||||
import info.nightscout.core.events.EventIobCalculationProgress
|
||||
import info.nightscout.core.graph.OverviewData
|
||||
import info.nightscout.core.graph.data.BolusDataPoint
|
||||
|
@ -10,6 +11,8 @@ import info.nightscout.core.graph.data.CarbsDataPoint
|
|||
import info.nightscout.core.graph.data.DataPointWithLabelInterface
|
||||
import info.nightscout.core.graph.data.EffectiveProfileSwitchDataPoint
|
||||
import info.nightscout.core.graph.data.ExtendedBolusDataPoint
|
||||
import info.nightscout.core.graph.data.FixedLineGraphSeries
|
||||
import info.nightscout.core.graph.data.HeartRateDataPoint
|
||||
import info.nightscout.core.graph.data.PointsWithLabelGraphSeries
|
||||
import info.nightscout.core.graph.data.TherapyEventDataPoint
|
||||
import info.nightscout.core.utils.receivers.DataWorkerStorage
|
||||
|
@ -129,6 +132,11 @@ class PrepareTreatmentsDataWorker(
|
|||
data.overviewData.therapyEventSeries = PointsWithLabelGraphSeries(filteredTherapyEvents.toTypedArray())
|
||||
data.overviewData.epsSeries = PointsWithLabelGraphSeries(filteredEps.toTypedArray())
|
||||
|
||||
data.overviewData.heartRateGraphSeries = LineGraphSeries<DataPointWithLabelInterface>(
|
||||
repository.getHeartRatesFromTimeToTime(fromTime, endTime)
|
||||
.map { hr -> HeartRateDataPoint(hr, rh) }
|
||||
.toTypedArray()).apply { color = rh.gac(null, info.nightscout.core.ui.R.attr.heartRateColor) }
|
||||
|
||||
rxBus.send(EventIobCalculationProgress(CalculationWorkflow.ProgressData.PREPARE_TREATMENTS_DATA, 100, null))
|
||||
return Result.success()
|
||||
}
|
||||
|
@ -149,4 +157,4 @@ class PrepareTreatmentsDataWorker(
|
|||
|
||||
private fun <E : DataPointWithLabelInterface> List<E>.filterTimeframe(fromTime: Long, endTime: Long): List<E> =
|
||||
filter { it.x + it.duration >= fromTime && it.x <= endTime }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue