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
app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata
app/src/main/java/info/nightscout/androidaps/activities
core
graph/src/main/java/info/nightscout/core/graph
interfaces/src/main/java/info/nightscout/interfaces/overview
ui/src/main/res
database/impl/src
androidTest/java/info/nightscout/database/impl
main/java/info/nightscout/database/impl
implementation/src/main/java/info/nightscout/implementation/overview
plugins/main/src/main
java/info/nightscout/plugins/general
overview
wear/wearintegration
res/values
wear/src
main
test/java/info/nightscout/androidaps/heartrate
workflow/src/main/java/info/nightscout/workflow
|
@ -3,6 +3,7 @@ package info.nightscout.rx.weardata
|
||||||
import info.nightscout.rx.events.Event
|
import info.nightscout.rx.events.Event
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.joda.time.DateTime
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -90,6 +91,16 @@ sealed class EventData : Event() {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ActionQuickWizardPreCheck(val guid: String) : EventData()
|
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
|
@Serializable
|
||||||
data class ActionTempTargetPreCheck(
|
data class ActionTempTargetPreCheck(
|
||||||
val command: TempTargetCommand,
|
val command: TempTargetCommand,
|
||||||
|
|
|
@ -327,6 +327,7 @@ class HistoryBrowseActivity : TranslatedDaggerAppCompatActivity() {
|
||||||
var useRatioForScale = false
|
var useRatioForScale = false
|
||||||
var useDSForScale = false
|
var useDSForScale = false
|
||||||
var useBGIForScale = false
|
var useBGIForScale = false
|
||||||
|
var useHRForScale = false
|
||||||
when {
|
when {
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.ABS.ordinal] -> useABSForScale = true
|
menuChartSettings[g + 1][OverviewMenus.CharType.ABS.ordinal] -> useABSForScale = true
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.IOB.ordinal] -> useIobForScale = 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.BGI.ordinal] -> useBGIForScale = true
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal] -> useRatioForScale = true
|
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal] -> useRatioForScale = true
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.DEVSLOPE.ordinal] -> useDSForScale = 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]
|
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.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.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.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
|
// set manual x bounds to have nice steps
|
||||||
secondGraphData.formatAxis(historyBrowserData.overviewData.fromTime, historyBrowserData.overviewData.endTime)
|
secondGraphData.formatAxis(historyBrowserData.overviewData.fromTime, historyBrowserData.overviewData.endTime)
|
||||||
|
|
|
@ -150,4 +150,7 @@ interface OverviewData {
|
||||||
val dsMinScale: Scale
|
val dsMinScale: Scale
|
||||||
var dsMaxSeries: LineGraphSeries<ScaledDataPoint>
|
var dsMaxSeries: LineGraphSeries<ScaledDataPoint>
|
||||||
var dsMinSeries: 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,
|
GENERAL_WITH_DURATION,
|
||||||
COB_FAIL_OVER,
|
COB_FAIL_OVER,
|
||||||
IOB_PREDICTION,
|
IOB_PREDICTION,
|
||||||
BUCKETED_BG
|
BUCKETED_BG,
|
||||||
|
HEARTRATE,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -324,6 +325,10 @@ public class PointsWithLabelGraphSeries<E extends DataPointWithLabelInterface> e
|
||||||
mPaint.setStrokeWidth(5);
|
mPaint.setStrokeWidth(5);
|
||||||
canvas.drawRect(endX - 3, bounds.top + py - 3, xPlusLength + 3, bounds.bottom + py + 3, mPaint);
|
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
|
// set values above point
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,8 @@ interface OverviewMenus {
|
||||||
BGI,
|
BGI,
|
||||||
SEN,
|
SEN,
|
||||||
ACT,
|
ACT,
|
||||||
DEVSLOPE
|
DEVSLOPE,
|
||||||
|
HR,
|
||||||
}
|
}
|
||||||
|
|
||||||
val setting: List<Array<Boolean>>
|
val setting: List<Array<Boolean>>
|
||||||
|
@ -23,4 +24,4 @@ interface OverviewMenus {
|
||||||
fun setupChartMenu(context: Context, chartButton: ImageButton)
|
fun setupChartMenu(context: Context, chartButton: ImageButton)
|
||||||
fun enabledTypes(graph: Int): String
|
fun enabledTypes(graph: Int): String
|
||||||
fun isEnabledIn(type: CharType): Int
|
fun isEnabledIn(type: CharType): Int
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,6 +216,7 @@
|
||||||
<item name="inRangeBackground">@color/inRangeBackground</item>
|
<item name="inRangeBackground">@color/inRangeBackground</item>
|
||||||
<item name="devSlopePosColor">@color/devSlopePos</item>
|
<item name="devSlopePosColor">@color/devSlopePos</item>
|
||||||
<item name="devSlopeNegColor">@color/devSlopeNeg</item>
|
<item name="devSlopeNegColor">@color/devSlopeNeg</item>
|
||||||
|
<item name="heartRateColor">@color/heartRate</item>
|
||||||
<item name="deviationGreyColor">@color/deviationGrey</item>
|
<item name="deviationGreyColor">@color/deviationGrey</item>
|
||||||
<item name="deviationBlackColor">@color/deviationBlack</item>
|
<item name="deviationBlackColor">@color/deviationBlack</item>
|
||||||
<item name="deviationGreenColor">@color/deviationGreen</item>
|
<item name="deviationGreenColor">@color/deviationGreen</item>
|
||||||
|
|
|
@ -181,6 +181,7 @@
|
||||||
<attr name="bolusDataPointColor" format="reference|color" />
|
<attr name="bolusDataPointColor" format="reference|color" />
|
||||||
<attr name="profileSwitchColor" format="reference|color" />
|
<attr name="profileSwitchColor" format="reference|color" />
|
||||||
<attr name="originalBgValueColor" 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_NS_MBG" format="reference|color" />
|
||||||
<attr name="therapyEvent_FINGER_STICK_BG_VALUE" format="reference|color" />
|
<attr name="therapyEvent_FINGER_STICK_BG_VALUE" format="reference|color" />
|
||||||
<attr name="therapyEvent_EXERCISE" format="reference|color" />
|
<attr name="therapyEvent_EXERCISE" format="reference|color" />
|
||||||
|
@ -221,4 +222,4 @@
|
||||||
<attr name="crossTargetColor" format="reference|color" />
|
<attr name="crossTargetColor" format="reference|color" />
|
||||||
<!---Custom button -->
|
<!---Custom button -->
|
||||||
<attr name="customBtnStyle" format="reference"/>
|
<attr name="customBtnStyle" format="reference"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -153,6 +153,7 @@
|
||||||
<color name="bgi">#00EEEE</color>
|
<color name="bgi">#00EEEE</color>
|
||||||
<color name="devSlopePos">#FFFFFF00</color>
|
<color name="devSlopePos">#FFFFFF00</color>
|
||||||
<color name="devSlopeNeg">#FFFF00FF</color>
|
<color name="devSlopeNeg">#FFFF00FF</color>
|
||||||
|
<color name="heartRate">#FFFFFF66</color>
|
||||||
<color name="actionsConfirm">#F6CE22</color>
|
<color name="actionsConfirm">#F6CE22</color>
|
||||||
<color name="deviations">#FF0000</color>
|
<color name="deviations">#FF0000</color>
|
||||||
<color name="cobAlert">#7484E2</color>
|
<color name="cobAlert">#7484E2</color>
|
||||||
|
|
|
@ -219,6 +219,7 @@
|
||||||
<item name="inRangeBackground">@color/inRangeBackground</item>
|
<item name="inRangeBackground">@color/inRangeBackground</item>
|
||||||
<item name="devSlopePosColor">@color/devSlopePos</item>
|
<item name="devSlopePosColor">@color/devSlopePos</item>
|
||||||
<item name="devSlopeNegColor">@color/devSlopeNeg</item>
|
<item name="devSlopeNegColor">@color/devSlopeNeg</item>
|
||||||
|
<item name="heartRateColor">@color/heartRate</item>
|
||||||
<item name="deviationGreyColor">@color/deviationGrey</item>
|
<item name="deviationGreyColor">@color/deviationGrey</item>
|
||||||
<item name="deviationBlackColor">@color/deviationBlack</item>
|
<item name="deviationBlackColor">@color/deviationBlack</item>
|
||||||
<item name="deviationGreenColor">@color/deviationGreen</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"
|
private const val TEST_DB_NAME = "testDatabase"
|
||||||
|
|
||||||
fun createHeartRate(timestamp: Long? = null, beatsPerMinute: Double = 80.0) =
|
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 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(
|
suspend fun collectNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int) = NewEntries(
|
||||||
apsResults = database.apsResultDao.getNewEntriesSince(since, until, limit, offset),
|
apsResults = database.apsResultDao.getNewEntriesSince(since, until, limit, offset),
|
||||||
apsResultLinks = database.apsResultLinkDao.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")
|
@Query("SELECT * FROM $TABLE_HEART_RATE WHERE timestamp >= :timestamp ORDER BY timestamp")
|
||||||
fun getFromTime(timestamp: Long): List<HeartRate>
|
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")
|
@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>
|
fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<HeartRate>
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,7 @@ class OverviewDataImpl @Inject constructor(
|
||||||
dsMinSeries = LineGraphSeries()
|
dsMinSeries = LineGraphSeries()
|
||||||
treatmentsSeries = PointsWithLabelGraphSeries()
|
treatmentsSeries = PointsWithLabelGraphSeries()
|
||||||
epsSeries = PointsWithLabelGraphSeries()
|
epsSeries = PointsWithLabelGraphSeries()
|
||||||
|
heartRateGraphSeries = LineGraphSeries()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initRange() {
|
override fun initRange() {
|
||||||
|
@ -322,4 +323,6 @@ class OverviewDataImpl @Inject constructor(
|
||||||
override val dsMinScale = Scale()
|
override val dsMinScale = Scale()
|
||||||
override var dsMaxSeries: LineGraphSeries<ScaledDataPoint> = LineGraphSeries()
|
override var dsMaxSeries: LineGraphSeries<ScaledDataPoint> = LineGraphSeries()
|
||||||
override var dsMinSeries: 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 useRatioForScale = false
|
||||||
var useDSForScale = false
|
var useDSForScale = false
|
||||||
var useBGIForScale = false
|
var useBGIForScale = false
|
||||||
|
var useHRForScale = false
|
||||||
when {
|
when {
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.ABS.ordinal] -> useABSForScale = true
|
menuChartSettings[g + 1][OverviewMenus.CharType.ABS.ordinal] -> useABSForScale = true
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.IOB.ordinal] -> useIobForScale = 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.BGI.ordinal] -> useBGIForScale = true
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal] -> useRatioForScale = true
|
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.ordinal] -> useRatioForScale = true
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.DEVSLOPE.ordinal] -> useDSForScale = 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]
|
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,
|
if (useDSForScale) 1.0 else 0.8,
|
||||||
useRatioForScale
|
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
|
// set manual x bounds to have nice steps
|
||||||
secondGraphData.formatAxis(overviewData.fromTime, overviewData.endTime)
|
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.DEV.ordinal] ||
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal] ||
|
menuChartSettings[g + 1][OverviewMenus.CharType.BGI.ordinal] ||
|
||||||
menuChartSettings[g + 1][OverviewMenus.CharType.SEN.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()
|
).toVisibility()
|
||||||
secondaryGraphsData[g].performUpdate()
|
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),
|
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),
|
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),
|
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 {
|
companion object {
|
||||||
|
@ -202,4 +203,4 @@ class OverviewMenusImpl @Inject constructor(
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,5 +258,14 @@ class GraphData(
|
||||||
// draw it
|
// draw it
|
||||||
graph.onDataChanged(false, false)
|
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.Bolus
|
||||||
import info.nightscout.database.entities.BolusCalculatorResult
|
import info.nightscout.database.entities.BolusCalculatorResult
|
||||||
import info.nightscout.database.entities.GlucoseValue
|
import info.nightscout.database.entities.GlucoseValue
|
||||||
|
import info.nightscout.database.entities.HeartRate
|
||||||
import info.nightscout.database.entities.TemporaryBasal
|
import info.nightscout.database.entities.TemporaryBasal
|
||||||
import info.nightscout.database.entities.TemporaryTarget
|
import info.nightscout.database.entities.TemporaryTarget
|
||||||
import info.nightscout.database.entities.TotalDailyDose
|
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.AppRepository
|
||||||
import info.nightscout.database.impl.transactions.CancelCurrentTemporaryTargetIfAnyTransaction
|
import info.nightscout.database.impl.transactions.CancelCurrentTemporaryTargetIfAnyTransaction
|
||||||
import info.nightscout.database.impl.transactions.InsertAndCancelCurrentTemporaryTargetTransaction
|
import info.nightscout.database.impl.transactions.InsertAndCancelCurrentTemporaryTargetTransaction
|
||||||
|
import info.nightscout.database.impl.transactions.InsertOrUpdateHeartRateTransaction
|
||||||
import info.nightscout.interfaces.Config
|
import info.nightscout.interfaces.Config
|
||||||
import info.nightscout.interfaces.Constants
|
import info.nightscout.interfaces.Constants
|
||||||
import info.nightscout.interfaces.GlucoseUnit
|
import info.nightscout.interfaces.GlucoseUnit
|
||||||
|
@ -308,6 +310,10 @@ class DataHandlerMobile @Inject constructor(
|
||||||
aapsLogger.debug(LTag.WEAR, "WearException received $it from ${it.sourceNodeId}")
|
aapsLogger.debug(LTag.WEAR, "WearException received $it from ${it.sourceNodeId}")
|
||||||
fabricPrivacy.logWearException(it)
|
fabricPrivacy.logWearException(it)
|
||||||
}, fabricPrivacy::logException)
|
}, fabricPrivacy::logException)
|
||||||
|
disposable += rxBus
|
||||||
|
.toObservable(EventData.ActionHeartRate::class.java)
|
||||||
|
.observeOn(aapsSchedulers.io)
|
||||||
|
.subscribe({ handleHeartRate(it) }, fabricPrivacy::logException)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleTddStatus() {
|
private fun handleTddStatus() {
|
||||||
|
@ -1230,4 +1236,15 @@ class DataHandlerMobile @Inject constructor(
|
||||||
@Synchronized private fun sendError(errorMessage: String) {
|
@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
|
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_predictions">Predictions</string>
|
||||||
<string name="overview_show_treatments">Treatments</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_deviation_slope">Deviation slope</string>
|
||||||
<string name="overview_show_activity">Activity</string>
|
<string name="overview_show_activity">Activity</string>
|
||||||
<string name="overview_show_bgi">Blood Glucose Impact</string>
|
<string name="overview_show_bgi">Blood Glucose Impact</string>
|
||||||
|
@ -308,6 +309,7 @@
|
||||||
<string name="abs_insulin_shortname">ABS</string>
|
<string name="abs_insulin_shortname">ABS</string>
|
||||||
<string name="devslope_shortname">DEVSLOPE</string>
|
<string name="devslope_shortname">DEVSLOPE</string>
|
||||||
<string name="treatments_shortname">TREAT</string>
|
<string name="treatments_shortname">TREAT</string>
|
||||||
|
<string name="heartRate_shortname">HR</string>
|
||||||
<string name="sensitivity_shortname">SENS</string>
|
<string name="sensitivity_shortname">SENS</string>
|
||||||
<string name="graph_scale">Graph scale</string>
|
<string name="graph_scale">Graph scale</string>
|
||||||
<string name="graph_menu_divider_header">Graph</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="com.google.android.permission.PROVIDE_BACKGROUND" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.BODY_SENSORS" />
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="androidx.wear.tiles" />
|
<uses-sdk tools:overrideLibrary="androidx.wear.tiles" />
|
||||||
|
|
||||||
|
@ -268,6 +269,10 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".heartrate.HeartRateListener"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".complications.LongStatusComplication"
|
android:name=".complications.LongStatusComplication"
|
||||||
android:icon="@drawable/ic_aaps_full"
|
android:icon="@drawable/ic_aaps_full"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import info.nightscout.androidaps.comm.DataLayerListenerServiceWear
|
import info.nightscout.androidaps.comm.DataLayerListenerServiceWear
|
||||||
import info.nightscout.androidaps.complications.*
|
import info.nightscout.androidaps.complications.*
|
||||||
|
import info.nightscout.androidaps.heartrate.HeartRateListener
|
||||||
import info.nightscout.androidaps.tile.*
|
import info.nightscout.androidaps.tile.*
|
||||||
import info.nightscout.androidaps.watchfaces.*
|
import info.nightscout.androidaps.watchfaces.*
|
||||||
import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace
|
import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace
|
||||||
|
@ -13,7 +14,7 @@ import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace
|
||||||
abstract class WearServicesModule {
|
abstract class WearServicesModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector abstract fun contributesDataLayerListenerService(): DataLayerListenerServiceWear
|
@ContributesAndroidInjector abstract fun contributesDataLayerListenerService(): DataLayerListenerServiceWear
|
||||||
|
@ContributesAndroidInjector abstract fun contributesHeartRateListenerService(): HeartRateListener
|
||||||
@ContributesAndroidInjector abstract fun contributesBaseComplicationProviderService(): BaseComplicationProviderService
|
@ContributesAndroidInjector abstract fun contributesBaseComplicationProviderService(): BaseComplicationProviderService
|
||||||
@ContributesAndroidInjector abstract fun contributesBrCobIobComplication(): BrCobIobComplication
|
@ContributesAndroidInjector abstract fun contributesBrCobIobComplication(): BrCobIobComplication
|
||||||
@ContributesAndroidInjector abstract fun contributesCobDetailedComplication(): CobDetailedComplication
|
@ContributesAndroidInjector abstract fun contributesCobDetailedComplication(): CobDetailedComplication
|
||||||
|
@ -45,4 +46,4 @@ abstract class WearServicesModule {
|
||||||
@ContributesAndroidInjector abstract fun contributesTempTargetTileService(): TempTargetTileService
|
@ContributesAndroidInjector abstract fun contributesTempTargetTileService(): TempTargetTileService
|
||||||
@ContributesAndroidInjector abstract fun contributesActionsTileService(): ActionsTileService
|
@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
|
package info.nightscout.androidaps.interaction
|
||||||
|
|
||||||
import preference.WearPreferenceActivity
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -9,7 +8,7 @@ import dagger.android.AndroidInjection
|
||||||
import info.nightscout.androidaps.R
|
import info.nightscout.androidaps.R
|
||||||
import info.nightscout.rx.logging.AAPSLogger
|
import info.nightscout.rx.logging.AAPSLogger
|
||||||
import info.nightscout.rx.logging.LTag
|
import info.nightscout.rx.logging.LTag
|
||||||
|
import preference.WearPreferenceActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ConfigurationActivity : WearPreferenceActivity() {
|
class ConfigurationActivity : WearPreferenceActivity() {
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
package info.nightscout.androidaps.interaction
|
package info.nightscout.androidaps.interaction
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import info.nightscout.androidaps.R
|
import info.nightscout.androidaps.R
|
||||||
|
import info.nightscout.rx.logging.AAPSLogger
|
||||||
|
import info.nightscout.rx.logging.LTag
|
||||||
import preference.WearPreferenceActivity
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
addPreferencesFromResource(R.xml.preferences)
|
addPreferencesFromResource(R.xml.preferences)
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
|
||||||
val view = window.decorView as ViewGroup
|
val view = window.decorView as ViewGroup
|
||||||
removeBackgroundRecursively(view)
|
removeBackgroundRecursively(view)
|
||||||
view.background = ContextCompat.getDrawable(this, R.drawable.settings_background)
|
view.background = ContextCompat.getDrawable(this, R.drawable.settings_background)
|
||||||
|
@ -24,4 +35,38 @@ class WatchfaceConfigurationActivity : WearPreferenceActivity() {
|
||||||
removeBackgroundRecursively(parent.getChildAt(i))
|
removeBackgroundRecursively(parent.getChildAt(i))
|
||||||
parent.background = null
|
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.R
|
||||||
import info.nightscout.androidaps.data.RawDisplayData
|
import info.nightscout.androidaps.data.RawDisplayData
|
||||||
import info.nightscout.androidaps.events.EventWearPreferenceChange
|
import info.nightscout.androidaps.events.EventWearPreferenceChange
|
||||||
|
import info.nightscout.androidaps.heartrate.HeartRateListener
|
||||||
import info.nightscout.androidaps.interaction.menus.MainMenuActivity
|
import info.nightscout.androidaps.interaction.menus.MainMenuActivity
|
||||||
import info.nightscout.androidaps.interaction.utils.Persistence
|
import info.nightscout.androidaps.interaction.utils.Persistence
|
||||||
import info.nightscout.androidaps.interaction.utils.WearUtil
|
import info.nightscout.androidaps.interaction.utils.WearUtil
|
||||||
|
@ -99,6 +100,7 @@ abstract class BaseWatchFace : WatchFace() {
|
||||||
|
|
||||||
private var mLastSvg = ""
|
private var mLastSvg = ""
|
||||||
private var mLastDirection = ""
|
private var mLastDirection = ""
|
||||||
|
private var heartRateListener: HeartRateListener? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
// Not derived from DaggerService, do injection here
|
// Not derived from DaggerService, do injection here
|
||||||
|
@ -115,6 +117,7 @@ abstract class BaseWatchFace : WatchFace() {
|
||||||
.subscribe { event: EventWearPreferenceChange ->
|
.subscribe { event: EventWearPreferenceChange ->
|
||||||
simpleUi.updatePreferences()
|
simpleUi.updatePreferences()
|
||||||
if (event.changedKey != null && event.changedKey == "delta_granularity") rxBus.send(EventWearToMobile(ActionResendData("BaseWatchFace:onSharedPreferenceChanged")))
|
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()
|
if (layoutSet) setDataFields()
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
@ -139,6 +142,7 @@ abstract class BaseWatchFace : WatchFace() {
|
||||||
layoutView = binding.root
|
layoutView = binding.root
|
||||||
performViewSetup()
|
performViewSetup()
|
||||||
rxBus.send(EventWearToMobile(ActionResendData("BaseWatchFace::onCreate")))
|
rxBus.send(EventWearToMobile(ActionResendData("BaseWatchFace::onCreate")))
|
||||||
|
updateHeartRateListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun forceUpdate() {
|
private fun forceUpdate() {
|
||||||
|
@ -146,6 +150,20 @@ abstract class BaseWatchFace : WatchFace() {
|
||||||
invalidate()
|
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) {
|
override fun onTapCommand(tapType: Int, x: Int, y: Int, eventTime: Long) {
|
||||||
binding.chart?.let { chart ->
|
binding.chart?.let { chart ->
|
||||||
if (tapType == TAP_TYPE_TAP && x >= chart.left && x <= chart.right && y >= chart.top && y <= chart.bottom) {
|
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">old</string>
|
||||||
<string name="old_warning">!old!</string>
|
<string name="old_warning">!old!</string>
|
||||||
<string name="error">!err!</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>
|
</resources>
|
||||||
|
|
|
@ -180,4 +180,11 @@
|
||||||
android:summary="Input Design"
|
android:summary="Input Design"
|
||||||
android:title="@string/pref_moreWatchfaceSettings" />
|
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>
|
</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 android.content.Context
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
|
import com.jjoe64.graphview.series.LineGraphSeries
|
||||||
import info.nightscout.core.events.EventIobCalculationProgress
|
import info.nightscout.core.events.EventIobCalculationProgress
|
||||||
import info.nightscout.core.graph.OverviewData
|
import info.nightscout.core.graph.OverviewData
|
||||||
import info.nightscout.core.graph.data.BolusDataPoint
|
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.DataPointWithLabelInterface
|
||||||
import info.nightscout.core.graph.data.EffectiveProfileSwitchDataPoint
|
import info.nightscout.core.graph.data.EffectiveProfileSwitchDataPoint
|
||||||
import info.nightscout.core.graph.data.ExtendedBolusDataPoint
|
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.PointsWithLabelGraphSeries
|
||||||
import info.nightscout.core.graph.data.TherapyEventDataPoint
|
import info.nightscout.core.graph.data.TherapyEventDataPoint
|
||||||
import info.nightscout.core.utils.receivers.DataWorkerStorage
|
import info.nightscout.core.utils.receivers.DataWorkerStorage
|
||||||
|
@ -129,6 +132,11 @@ class PrepareTreatmentsDataWorker(
|
||||||
data.overviewData.therapyEventSeries = PointsWithLabelGraphSeries(filteredTherapyEvents.toTypedArray())
|
data.overviewData.therapyEventSeries = PointsWithLabelGraphSeries(filteredTherapyEvents.toTypedArray())
|
||||||
data.overviewData.epsSeries = PointsWithLabelGraphSeries(filteredEps.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))
|
rxBus.send(EventIobCalculationProgress(CalculationWorkflow.ProgressData.PREPARE_TREATMENTS_DATA, 100, null))
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
@ -149,4 +157,4 @@ class PrepareTreatmentsDataWorker(
|
||||||
|
|
||||||
private fun <E : DataPointWithLabelInterface> List<E>.filterTimeframe(fromTime: Long, endTime: Long): List<E> =
|
private fun <E : DataPointWithLabelInterface> List<E>.filterTimeframe(fromTime: Long, endTime: Long): List<E> =
|
||||||
filter { it.x + it.duration >= fromTime && it.x <= endTime }
|
filter { it.x + it.duration >= fromTime && it.x <= endTime }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue