Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ kotlin {

implementation(libs.androidx.datastore.core)
implementation(libs.androidx.datastore.preferences)
implementation(libs.commons.math3)

configurations["kspAndroid"].dependencies.add(project.dependencies.create(libs.hilt.android.compiler.get()))
}
Expand Down Expand Up @@ -74,6 +75,7 @@ kotlin {
implementation(libs.androidx.room.common)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
implementation(libs.commons.math3)
}

val desktopMain by getting {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,16 @@ class MainActivity : ComponentActivity() {
groupDelay = settingsViewModel.groupDelay.asStateWrapper(),
autoIgnoreEnabled = settingsViewModel.autoIgnoreEnabled.asStateWrapper(),
autoIgnoreValue = settingsViewModel.autoIgnoreValue.asStateWrapper(),
groupIoB = settingsViewModel.groupIoB.asStateWrapper(),
insulinPeak = settingsViewModel.insulinPeak.asStateWrapper(),
delta = settingsViewModel.delta.asStateWrapper(),
insulinDuration = settingsViewModel.insulinDuration.asStateWrapper()
)
)
} else {
MainScreen(
doseList = viewModel.doseList.collectAsState(listOf()).value.reversed(),
iob = viewModel.iob.collectAsState(null).value,
message = viewModel.getReadMessage().collectAsState().value,

loading = viewModel.isReading().collectAsState().value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import net.cacheux.nvp.app.utils.csvToDoseList
import net.cacheux.nvp.logging.logDebug
import net.cacheux.nvp.model.Dose
import net.cacheux.nvp.model.DoseGroup
import net.cacheux.nvp.model.IoB
import java.io.InputStream
import java.util.concurrent.TimeUnit
import javax.inject.Inject

@HiltViewModel
Expand All @@ -33,6 +37,8 @@ class MainScreenViewModel @Inject constructor(
preferencesRepository
)

private val iobUseCase: IoBUseCase = IoBUseCase(preferencesRepository)

fun getPenList() = storageRepository.getPenList()

private val isReading = MutableStateFlow(false)
Expand Down Expand Up @@ -64,6 +70,14 @@ class MainScreenViewModel @Inject constructor(
storageRepository.getDoseList(it)
}

val iob: Flow<IoB?> = iobUseCase.calculate(doseList, flow {
while(true) {
emit(System.currentTimeMillis())
delay(TimeUnit.MINUTES.toMillis(1))
}
})


val store = repository.getDataStore()

fun loadCsvFile(input: InputStream) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ class SettingsViewModel @Inject constructor(
val groupDelay = preferencesRepository.groupDelay
val autoIgnoreEnabled = preferencesRepository.autoIgnoreEnabled
val autoIgnoreValue = preferencesRepository.autoIgnoreValue

val groupIoB = preferencesRepository.groupIoB
val insulinPeak = preferencesRepository.insulinPeak
val delta = preferencesRepository.delta
val insulinDuration = preferencesRepository.insulinDuration
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class DatastorePreferencesRepository(context: Context): PreferencesRepository {
val GROUP_DELAY = intPreferencesKey("group_delay")
val AUTO_IGNORE_ENABLED = booleanPreferencesKey("auto_ignore_enabled")
val AUTO_IGNORE_VALUE = intPreferencesKey("auto_ignore_value")
val GROUP_IOB = booleanPreferencesKey("group_iob")
val IOB_INSULIN_PEAK = intPreferencesKey("iob_insulin_peak")
val IOB_DELTA = intPreferencesKey("iob_delta")
val IOB_INSULIN_DURATION = intPreferencesKey("iob_insulin_duration")
}

override val groupEnabled: StateFlowWrapper<Boolean> =
Expand All @@ -39,4 +43,20 @@ class DatastorePreferencesRepository(context: Context): PreferencesRepository {
PreferenceStateFlowWrapper(
dataStore, PreferencesKeys.AUTO_IGNORE_VALUE, 2
)

override val groupIoB: StateFlowWrapper<Boolean> = PreferenceStateFlowWrapper(
dataStore, PreferencesKeys.GROUP_IOB, false
)

override val insulinPeak: StateFlowWrapper<Int> = PreferenceStateFlowWrapper(
dataStore, PreferencesKeys.IOB_INSULIN_PEAK, 75
)

override val delta: StateFlowWrapper<Int> = PreferenceStateFlowWrapper(
dataStore, PreferencesKeys.IOB_DELTA, 15
)

override val insulinDuration: StateFlowWrapper<Int> = PreferenceStateFlowWrapper(
dataStore, PreferencesKeys.IOB_INSULIN_DURATION, 5
)
}
89 changes: 89 additions & 0 deletions app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package net.cacheux.nvp.app

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import net.cacheux.nvp.model.DoseGroup
import net.cacheux.nvp.model.InsulinUnit
import net.cacheux.nvp.model.IoB
import java.util.concurrent.TimeUnit
import org.apache.commons.math3.special.Gamma

class IoBUseCase(
private val preferencesRepository: PreferencesRepository,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) {
// How fast insulin is absorbed (Novorapid: 1.5)
val absorptionRate: Double = 1.5


fun calculate(doseGroups: Flow<List<DoseGroup>>, time: Flow<Long>): Flow<IoB?> {
return combine(
doseGroups,
time,
preferencesRepository.groupIoB.content,
preferencesRepository.insulinPeak.content.map { TimeUnit.MINUTES.toMillis(it.toLong()) },
preferencesRepository.delta.content.map { TimeUnit.MINUTES.toMillis(it.toLong()) },
preferencesRepository.insulinDuration.content.map { TimeUnit.HOURS.toMillis(it.toLong()) },
) { values ->
val doseGroups = values[0] as List<DoseGroup>
val time = values[1] as Long
val enabled = values[2] as Boolean
val insulinPeak = values[3] as Long
val delta = values[4] as Long
val insulinDuration = values[5] as Long

if (!enabled) {
return@combine null
}

val nextTime = time + delta

val remaining =
doseGroups
.sortedByDescending { it.getTime() }
.takeWhile { time - it.getTime() <= insulinDuration }
.map {
(it.getTotal() * this.fraction(
time,
it.getTime(),
insulinPeak
)).toInt()
}

val next =
doseGroups
.sortedByDescending { it.getTime() }
.takeWhile { nextTime - it.getTime() <= insulinDuration }
.map {
(it.getTotal() * (this.fraction(
time,
it.getTime(),
insulinPeak
) - this.fraction(nextTime, it.getTime(), insulinPeak))).toInt()
}

IoB(
time = time,
remaining = InsulinUnit(remaining.sum()),
serial = doseGroups.lastOrNull()?.getSerial() ?: "",
current = InsulinUnit(next.sum()),
delta = delta
)
}
}

fun fraction(
time: Long,
doseTime: Long,
insulinPeak: Long,
): Double {
val timeSinceBolus = time - doseTime
val x = absorptionRate * timeSinceBolus / insulinPeak
val r = Gamma.regularizedGammaQ(absorptionRate + 1, x)

return r
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,12 @@ interface PreferencesRepository {
val groupDelay: StateFlowWrapper<Int>
val autoIgnoreEnabled: StateFlowWrapper<Boolean>
val autoIgnoreValue: StateFlowWrapper<Int>

val groupIoB: StateFlowWrapper<Boolean>
// Time to peak insulin activity (Novorapid: 75 minutes)
val insulinPeak: StateFlowWrapper<Int>
// Time period to calculate amount of current active insulin
val delta: StateFlowWrapper<Int>
// How old doses is calculated (Novorapid: 5 hours)
val insulinDuration: StateFlowWrapper<Int>
}
6 changes: 5 additions & 1 deletion app/src/desktopMain/kotlin/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import net.cacheux.nvp.app.DoseListUseCase
import net.cacheux.nvp.app.IoBUseCase
import net.cacheux.nvp.app.MainScreenViewModel
import net.cacheux.nvp.app.SettingsViewModel
import net.cacheux.nvp.app.StorageRepository
Expand Down Expand Up @@ -42,7 +43,8 @@ val preferencesRepository = PreferencesRepositoryImpl()
val mainScreenViewModel = MainScreenViewModel(
TestPenInfoRepository(),
doseListUseCase = DoseListUseCase(storageRepository, preferencesRepository),
storageRepository = storageRepository
storageRepository = storageRepository,
iobUseCase = IoBUseCase(preferencesRepository)
)

val settingsViewModel = SettingsViewModel(preferencesRepository)
Expand Down Expand Up @@ -93,6 +95,8 @@ fun main() = application {
} else {
MainScreen(
doseList = mainScreenViewModel.doseList.collectAsState(listOf()).value.reversed(),
iob = mainScreenViewModel.iob.collectAsState(null).value,

loadingFileAvailable = true,

loading = mainScreenViewModel.isReading().collectAsState().value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@ import io.github.vinceglb.filekit.core.PlatformFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import net.cacheux.nvp.app.utils.csvToDoseList
import net.cacheux.nvp.logging.logDebug
import net.cacheux.nvp.model.Dose
import net.cacheux.nvp.model.DoseGroup
import net.cacheux.nvp.model.IoB
import net.cacheux.nvplib.NvpController
import net.cacheux.nvplib.testing.TestingDataReader
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit

class MainScreenViewModel(
private val repository: PenInfoRepository,
private val storageRepository: StorageRepository,
private val doseListUseCase: DoseListUseCase,
private val iobUseCase: IoBUseCase,
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) {
fun getPenList() = storageRepository.getPenList()
Expand Down Expand Up @@ -54,6 +59,13 @@ class MainScreenViewModel(
storageRepository.getDoseList(it)
}

val iob: Flow<IoB?> = iobUseCase.calculate(doseList, flow {
while(true) {
emit(System.currentTimeMillis())
delay(TimeUnit.MINUTES.toMillis(1))
}
})

val store = repository.getDataStore()

fun loadCsvFile(file: PlatformFile) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class PreferencesRepositoryImpl: PreferencesRepository {
override val autoIgnoreEnabled = booleanStateFlowWrapper("autoIgnoreEnabled", "true")
override val autoIgnoreValue = intStateFlowWrapper("autoIgnoreValue", "2")

override val groupIoB = booleanStateFlowWrapper("groupIoB", "false")
override val insulinPeak = intStateFlowWrapper("insulinPeak", "75")
override val delta = intStateFlowWrapper("delta", "15")
override val insulinDuration = intStateFlowWrapper("insulinDuration", "5")

private fun saveProperties() {
propertiesFile.outputStream().use { properties.store(it, null) }
}
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ compose-compiler = "1.5.14"
room = "2.7.0-alpha11"
sqlite = "2.5.0-SNAPSHOT"
androidx-datastore = "1.1.1"
commons-math = "3.6.1"


[plugins]
ksp = { id = "com.google.devtools.ksp", version = "1.9.24-1.0.20" }
Expand All @@ -35,6 +37,7 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.7.0
androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.9.3" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "androidx-datastore" }
androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "androidx-datastore" }
commons-math3 = { group = "org.apache.commons", name = "commons-math3", version.ref = "commons-math" }
# Room
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-common = { group = "androidx.room", name = "room-common", version.ref = "room" }
Expand Down
2 changes: 1 addition & 1 deletion model/src/commonMain/kotlin/net/cacheux/nvp/model/Dose.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ data class Dose(
): DatedItem {
fun ignored() = Dose(time, value, true, serial)

fun displayedValue() = String.format("%.1f", value.toFloat() / 10)
fun displayedValue() = String.format("%.1f", InsulinUnit(value).toFloat())

/**
* Compare with another without the ignored field
Expand Down
16 changes: 16 additions & 0 deletions model/src/commonMain/kotlin/net/cacheux/nvp/model/InsulinUnit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.cacheux.nvp.model

@JvmInline
value class InsulinUnit(private val v: Int) {
constructor(f: Float) : this((f * 10).toInt())
constructor(f: Double) : this((f * 10).toInt())

fun toFloat() = v.toFloat() / 10
fun toInt() = v

operator fun minus(other: InsulinUnit): InsulinUnit {
return InsulinUnit(this.v - other.v)
}
}


9 changes: 9 additions & 0 deletions model/src/commonMain/kotlin/net/cacheux/nvp/model/IoB.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.cacheux.nvp.model

data class IoB(
val time: Long,
val remaining: InsulinUnit,
val current: InsulinUnit,
val delta: Long,
val serial: String = ""
)
12 changes: 12 additions & 0 deletions ui/src/commonMain/composeResources/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<string name="export_csv">Exporter au format CSV</string>
<string name="import_csv">Importer un fichier CSV</string>
<string name="back_button">Bouton retour</string>
<string name="insulin_on_board">Insuline à bord :</string>
<string name="active_insulin">Active :</string>

<string name="ok">OK</string>
<string name="cancel">Annuler</string>
Expand All @@ -24,4 +26,14 @@
<string name="auto_ignore_details">Les valeurs en dessous de la valeur spécifiée ne seront pas comptées</string>
<string name="auto_ignore_value">En dessous de</string>
<string name="auto_ignore_value_suffix"> unités</string>

<string name="group_iob">Insuline à bord</string>
<string name="group_iob_details">Afficher l'insuline active</string>
<string name="group_iob_insulin_peak">Période de pic d'insuline (min)</string>
<string name="group_iob_delta">Période de calcul de l'insuline active (min)</string>
<string name="group_iob_insulin_duration">Durée de l'insuline (heures)</string>

<string name="minutes_suffix"> minutes</string>
<string name="hours_suffix"> heures</string>

</resources>
Loading