From d3574f09410e7213afc65e94b43ebf16cc51f189 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Wed, 19 Feb 2025 06:59:01 +0300 Subject: [PATCH 01/29] Initial Apex TruCare III plugin --- app/build.gradle.kts | 1 + .../main/kotlin/app/aaps/di/AppComponent.kt | 2 + .../kotlin/app/aaps/di/PluginsListModule.kt | 7 + .../aaps/core/data/pump/defs/DoseStepSize.kt | 7 + .../core/data/pump/defs/ManufacturerType.kt | 4 +- .../core/data/pump/defs/PumpCapability.kt | 1 + .../core/data/pump/defs/PumpDescription.kt | 2 +- .../app/aaps/core/data/pump/defs/PumpType.kt | 19 +- .../entities/embedments/InterfaceIDs.kt | 3 +- .../converters/PumpTypeExtension.kt | 2 + pump/apex/.gitignore | 1 + pump/apex/build.gradle.kts | 31 + pump/apex/proguard-rules.pro | 21 + pump/apex/src/main/AndroidManifest.xml | 17 + .../kotlin/app/aaps/pump/apex/ApexPump.kt | 216 ++++ .../app/aaps/pump/apex/ApexPumpPlugin.kt | 499 ++++++++ .../kotlin/app/aaps/pump/apex/ApexService.kt | 1039 +++++++++++++++++ .../pump/apex/connectivity/ApexBluetooth.kt | 309 +++++ .../pump/apex/connectivity/ProtocolVersion.kt | 15 + .../apex/connectivity/commands/CommandId.kt | 7 + .../commands/device/BaseValueCommand.kt | 27 + .../connectivity/commands/device/Bolus.kt | 21 + .../commands/device/CancelBolus.kt | 17 + .../commands/device/CancelTemporaryBasal.kt | 11 + .../commands/device/DeviceCommand.kt | 40 + .../connectivity/commands/device/DualBolus.kt | 25 + .../commands/device/ExtendedBolus.kt | 23 + .../connectivity/commands/device/GetValue.kt | 56 + .../commands/device/NotifyAboutConnection.kt | 16 + .../commands/device/SyncDateTime.kt | 29 + .../commands/device/TemporaryBasal.kt | 26 + .../device/UpdateBasalProfileRates.kt | 26 + .../commands/device/UpdateSettingsV1.kt | 89 ++ .../commands/device/UpdateSystemState.kt | 21 + .../commands/device/UpdateUsedBasalProfile.kt | 20 + .../connectivity/commands/pump/AlarmObject.kt | 23 + .../commands/pump/BasalProfile.kt | 13 + .../connectivity/commands/pump/BolusEntry.kt | 43 + .../commands/pump/CommandResponse.kt | 20 + .../connectivity/commands/pump/Heartbeat.kt | 3 + .../connectivity/commands/pump/PumpCommand.kt | 63 + .../connectivity/commands/pump/PumpObjects.kt | 105 ++ .../connectivity/commands/pump/StatusV1.kt | 154 +++ .../connectivity/commands/pump/TDDEntry.kt | 30 + .../connectivity/commands/pump/Version.kt | 34 + .../app/aaps/pump/apex/di/ApexModule.kt | 9 + .../aaps/pump/apex/di/ApexServicesModule.kt | 18 + .../app/aaps/pump/apex/di/ApexUiModule.kt | 11 + .../apex/events/EventApexPumpDataChanged.kt | 5 + .../apex/interfaces/ApexBluetoothCallback.kt | 9 + .../pump/apex/interfaces/ApexDeviceInfo.kt | 5 + .../aaps/pump/apex/misc/ApexDeviceInfoImpl.kt | 16 + .../app/aaps/pump/apex/ui/ApexFragment.kt | 114 ++ .../app/aaps/pump/apex/utils/ApexCrypto.kt | 22 + .../app/aaps/pump/apex/utils/DataUtils.kt | 31 + .../pump/apex/utils/keys/ApexBooleanKey.kt | 20 + .../pump/apex/utils/keys/ApexDoubleKey.kt | 23 + .../pump/apex/utils/keys/ApexStringKey.kt | 24 + pump/apex/src/main/res/drawable/ic_apex.xml | 63 + .../src/main/res/layout/apex_fragment.xml | 404 +++++++ pump/apex/src/main/res/values/strings.xml | 86 ++ .../kotlin/app/aaps/pump/apex/CommandsTest.kt | 144 +++ settings.gradle | 1 + 63 files changed, 4138 insertions(+), 5 deletions(-) create mode 100644 pump/apex/.gitignore create mode 100644 pump/apex/build.gradle.kts create mode 100644 pump/apex/proguard-rules.pro create mode 100644 pump/apex/src/main/AndroidManifest.xml create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ProtocolVersion.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/CommandId.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/BaseValueCommand.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/Bolus.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelBolus.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelTemporaryBasal.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DeviceCommand.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DualBolus.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/ExtendedBolus.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/GetValue.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/NotifyAboutConnection.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/SyncDateTime.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/TemporaryBasal.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateBasalProfileRates.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSystemState.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateUsedBasalProfile.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/AlarmObject.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BasalProfile.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BolusEntry.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/CommandResponse.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Heartbeat.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/TDDEntry.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexModule.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexServicesModule.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexUiModule.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/events/EventApexPumpDataChanged.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexBluetoothCallback.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexDeviceInfo.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/misc/ApexDeviceInfoImpl.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/ui/ApexFragment.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/ApexCrypto.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/DataUtils.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexBooleanKey.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexDoubleKey.kt create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexStringKey.kt create mode 100644 pump/apex/src/main/res/drawable/ic_apex.xml create mode 100644 pump/apex/src/main/res/layout/apex_fragment.xml create mode 100644 pump/apex/src/main/res/values/strings.xml create mode 100644 pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61219b53976f..0552f4bee767 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -195,6 +195,7 @@ dependencies { implementation(project(":implementation")) implementation(project(":database:impl")) implementation(project(":database:persistence")) + implementation(project(":pump:apex")) implementation(project(":pump:combov2")) implementation(project(":pump:dana")) implementation(project(":pump:danars")) diff --git a/app/src/main/kotlin/app/aaps/di/AppComponent.kt b/app/src/main/kotlin/app/aaps/di/AppComponent.kt index 89703326da1b..9f47a5649789 100644 --- a/app/src/main/kotlin/app/aaps/di/AppComponent.kt +++ b/app/src/main/kotlin/app/aaps/di/AppComponent.kt @@ -15,6 +15,7 @@ import app.aaps.plugins.main.di.PluginsModule import app.aaps.plugins.source.di.SourceModule import app.aaps.plugins.sync.di.OpenHumansModule import app.aaps.plugins.sync.di.SyncModule +import app.aaps.pump.apex.di.ApexModule import app.aaps.pump.common.di.PumpCommonModule import app.aaps.pump.dana.di.DanaHistoryModule import app.aaps.pump.dana.di.DanaModule @@ -88,6 +89,7 @@ import javax.inject.Singleton RileyLinkModule::class, MedtrumModule::class, EquilModule::class, + ApexModule::class, VirtualPumpModule::class ] ) diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt index 30570b41c1de..d8c92c2c66e4 100644 --- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt +++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt @@ -67,6 +67,7 @@ import app.aaps.pump.medtrum.MedtrumPlugin import app.aaps.pump.omnipod.dash.OmnipodDashPumpPlugin import app.aaps.pump.omnipod.eros.OmnipodErosPumpPlugin import app.aaps.pump.virtual.VirtualPumpPlugin +import app.aaps.pump.apex.ApexPumpPlugin import dagger.Binds import dagger.Module import dagger.multibindings.IntKey @@ -222,6 +223,12 @@ abstract class PluginsListModule { @IntKey(170) abstract fun bindEquilPumpPlugin(plugin: EquilPumpPlugin): PluginBase + @Binds + @PumpDriver + @IntoMap + @IntKey(171) + abstract fun bindApexPumpPlugin(plugin: ApexPumpPlugin): PluginBase + @Binds @AllConfigs @IntoMap diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt index e0078146a641..e77fb81a97bd 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt @@ -39,6 +39,13 @@ enum class DoseStepSize(private val entries: Array) { DoseStepSizeEntry(2.0, 15.0, 0.1), DoseStepSizeEntry(15.0, 40.0, 0.5) ) + ), + Apex( + arrayOf( + DoseStepSizeEntry(0.0, 1.0, 0.025), + DoseStepSizeEntry(1.0, 2.0, 0.05), + DoseStepSizeEntry(2.0, Double.MAX_VALUE, 0.1) + ) ); fun getStepSizeForAmount(amount: Double): Double { diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt index 51daa400c22b..603727ecc0e6 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt @@ -13,6 +13,6 @@ enum class ManufacturerType(val description: String) { Ypsomed("Ypsomed"), G2e("G2e"), Eoflow("Eoflow"), - Equil("Equil"); - + Equil("Equil"), + Apex("APEX"); } \ No newline at end of file diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt index 36dc916fa9a6..c4bc5bf6f2db 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt @@ -20,6 +20,7 @@ enum class PumpCapability { DiaconnCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.Refill, Capability.ReplaceBattery, Capability.TDD, Capability.ManualTDDLoad)), // EopatchCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min)), MedtrumCapabilities(arrayOf(Capability.Bolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min, Capability.TDD)), // Technically the pump supports ExtendedBolus, but not implemented (yet) + ApexCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min, Capability.TDD, Capability.ManualTDDLoad, Capability.ReplaceBattery, Capability.Refill)) ; var children: ArrayList = ArrayList() diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt index 94c2ba0ce922..fb0463d43bc1 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.pump.defs -class PumpDescription { +open class PumpDescription { var pumpType = PumpType.GENERIC_AAPS var isBolusCapable = false diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpType.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpType.kt index 2de1a2c3f2b4..c212fac9bab9 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpType.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpType.kt @@ -474,6 +474,22 @@ enum class PumpType( pumpCapability = PumpCapability.DiaconnCapabilities, source = Source.EQuil, useHardwareLink = true, + ), + APEX_TRUCARE_III( + description = "APEX TruCare III", + manufacturer = ManufacturerType.Apex, + model = "TruCare III", + extendedBolusSettings = DoseSettings(0.025, 15, 24 * 60, 0.025), + tbrSettings = DoseSettings(0.025, 15, 24 * 60, 0.0), + pumpTempBasalType = PumpTempBasalType.Absolute, + bolusSize = 0.025, + baseBasalMinValue = 0.025, + baseBasalStep = 0.025, + baseBasalSpecialSteps = DoseStepSize.Apex, + specialBolusSize = DoseStepSize.Apex, + pumpCapability = PumpCapability.ApexCapabilities, + source = Source.ApexTruCareIII, + useHardwareLink = true, ); fun manufacturer() = parent?.manufacturer ?: manufacturer ?: throw IllegalStateException() @@ -511,7 +527,8 @@ enum class PumpType( MDI, VirtualPump, Unknown, - EQuil + EQuil, + ApexTruCareIII } companion object { diff --git a/database/impl/src/main/kotlin/app/aaps/database/entities/embedments/InterfaceIDs.kt b/database/impl/src/main/kotlin/app/aaps/database/entities/embedments/InterfaceIDs.kt index 078c6cb40ca6..2847fcb1e3fd 100644 --- a/database/impl/src/main/kotlin/app/aaps/database/entities/embedments/InterfaceIDs.kt +++ b/database/impl/src/main/kotlin/app/aaps/database/entities/embedments/InterfaceIDs.kt @@ -53,7 +53,8 @@ data class InterfaceIDs @Ignore constructor( MEDTRUM_UNTESTED, USER, CACHE, - EQUIL; + EQUIL, + APEX_TRUCARE_III; companion object { diff --git a/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/PumpTypeExtension.kt b/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/PumpTypeExtension.kt index e17b554e168d..053efd0ddc2d 100644 --- a/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/PumpTypeExtension.kt +++ b/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/PumpTypeExtension.kt @@ -42,6 +42,7 @@ fun InterfaceIDs.PumpType.fromDb(): PumpType = InterfaceIDs.PumpType.MEDTRUM_UNTESTED -> PumpType.MEDTRUM_UNTESTED InterfaceIDs.PumpType.CACHE -> PumpType.CACHE InterfaceIDs.PumpType.EQUIL -> PumpType.EQUIL + InterfaceIDs.PumpType.APEX_TRUCARE_III -> PumpType.APEX_TRUCARE_III } fun PumpType.toDb(): InterfaceIDs.PumpType = @@ -83,5 +84,6 @@ fun PumpType.toDb(): InterfaceIDs.PumpType = PumpType.MEDTRUM_UNTESTED -> InterfaceIDs.PumpType.MEDTRUM_UNTESTED PumpType.CACHE -> InterfaceIDs.PumpType.CACHE PumpType.EQUIL -> InterfaceIDs.PumpType.EQUIL + PumpType.APEX_TRUCARE_III -> InterfaceIDs.PumpType.APEX_TRUCARE_III } diff --git a/pump/apex/.gitignore b/pump/apex/.gitignore new file mode 100644 index 000000000000..796b96d1c402 --- /dev/null +++ b/pump/apex/.gitignore @@ -0,0 +1 @@ +/build diff --git a/pump/apex/build.gradle.kts b/pump/apex/build.gradle.kts new file mode 100644 index 000000000000..6765abb1e313 --- /dev/null +++ b/pump/apex/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(libs.plugins.android.library) + id("kotlin-android") + id("kotlin-kapt") + id("android-module-dependencies") + id("test-module-dependencies") + id("jacoco-module-dependencies") +} + +android { + namespace = "app.aaps.pump.apex" + buildFeatures { + dataBinding = true + } +} + +dependencies { + implementation(project(":core:data")) + implementation(project(":core:interfaces")) + implementation(project(":core:keys")) + implementation(project(":core:libraries")) + implementation(project(":core:objects")) + implementation(project(":core:ui")) + implementation(project(":core:utils")) + implementation(project(":core:validators")) + implementation(project(":pump:pump-common")) + testImplementation(project(":shared:tests")) + + kapt(libs.com.google.dagger.compiler) + kapt(libs.com.google.dagger.android.processor) +} \ No newline at end of file diff --git a/pump/apex/proguard-rules.pro b/pump/apex/proguard-rules.pro new file mode 100644 index 000000000000..f1b424510da5 --- /dev/null +++ b/pump/apex/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/pump/apex/src/main/AndroidManifest.xml b/pump/apex/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..122ecc654319 --- /dev/null +++ b/pump/apex/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt new file mode 100644 index 000000000000..779ca4215088 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt @@ -0,0 +1,216 @@ +package app.aaps.pump.apex + +import app.aaps.core.interfaces.pump.DetailedBolusInfo +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress +import app.aaps.pump.apex.connectivity.commands.pump.Alarm +import app.aaps.pump.apex.connectivity.commands.pump.BolusEntry +import app.aaps.pump.apex.connectivity.commands.pump.StatusV1 +import app.aaps.pump.apex.connectivity.commands.pump.Version +import org.joda.time.DateTime +import javax.inject.Inject +import javax.inject.Singleton + +/** + * @author Roman Rikhter (teledurak@gmail.com) + */ +@Singleton +class ApexPump @Inject constructor() { + private var _status: PumpStatus? = null + val status: PumpStatus? + get() = _status + + val batteryLevel: BatteryLevel + get() = status?.batteryLevel ?: BatteryLevel(0, 0, true) + + val isAdvancedBolusEnabled: Boolean + get() = status?.isAdvancedBolusEnabled ?: false + + val currentBasalPattern: Int + get() = status?.currentBasalPattern ?: 0 + + val maxBasal: Double + get() = status?.maxBasal ?: 0.0 + + val maxBolus: Double + get() = status?.maxBolus ?: 0.0 + + val dateTime: DateTime + get() = status?.dateTime ?: DateTime(0) + + val reservoirLevel: Double + get() = status?.reservoirLevel ?: 0.0 + + val alarms: List + get() = status?.alarms ?: listOf() + + val tbr: TBR? + get() = status?.tbr + + val basal: Basal? + get() = status?.basal + + val isSuspended: Boolean + get() = basal == null + + val isTBRunning: Boolean + get() = tbr != null + + val settingsAreUnadvised: Boolean + get() = isAdvancedBolusEnabled || currentBasalPattern != ApexService.USED_BASAL_PATTERN_INDEX + + val isV1: Boolean + get() = lastV1 != null + + var lastV1: StatusV1? = null + + var inProgressBolus: InProgressBolus? = null + var isAlarmPresent: Boolean = false + var lastBolus: BolusEntry? = null + var firmwareVersion: Version? = null + var serialNumber: String = "" + var gettingReady: Boolean = true + + val isBolusing: Boolean + get() = inProgressBolus != null + + fun updateFromV1(obj: StatusV1): StatusUpdate { + val updates = arrayListOf() + val new = PumpStatus( + batteryLevel = BatteryLevel(obj.batteryLevel!!.approximatePercentage, null, true), + isAdvancedBolusEnabled = obj.advancedBolusEnabled, + currentBasalPattern = obj.currentBasalPattern, + maxBasal = obj.maxBasal * 0.025, + maxBolus = obj.maxBolus * 0.025, + tbr = if (obj.isTemporaryBasalActive) TBR( + if (obj.temporaryBasalRateIsAbsolute) obj.temporaryBasalRate * 0.025 else null, + if (obj.temporaryBasalRateIsAbsolute) null else obj.temporaryBasalRate, + obj.temporaryBasalRateIsAbsolute, + obj.temporaryBasalRateDuration, + obj.temporaryBasalRateElapsed, + ) else null, + basal = if (obj.currentBasalRate == UShort.MAX_VALUE.toInt()) + null + else Basal( + obj.currentBasalRate * 0.025, + obj.currentBasalEndHour, + obj.currentBasalEndMinute, + ), + dateTime = obj.dateTime, + reservoirLevel = obj.reservoir / 1000.0, + alarms = obj.alarms + ) + + updates.apply { when { + new.basal != basal -> add(Update.BasalChanged) + new.tbr != tbr -> add(Update.TBRChanged) + new.maxBasal != maxBasal || new.maxBolus != maxBolus -> add(Update.ConstraintsChanged) + new.currentBasalPattern != currentBasalPattern || new.isAdvancedBolusEnabled != isAdvancedBolusEnabled -> add(Update.UnadvisedSettingsChanged) + new.batteryLevel != batteryLevel -> add(Update.BatteryChanged) + new.reservoirLevel != reservoirLevel -> add(Update.ReservoirChanged) + new.alarms != alarms -> add(Update.AlarmsChanged) + } } + + lastV1 = obj + + val ret = StatusUpdate( + changes = updates, + previous = status, + current = new, + ) + _status = new + return ret + } + + data class BatteryLevel( + val percentage: Int, + val voltage: Int?, // v2+ + val approximate: Boolean + ) + + data class Basal( + val rate: Double, + val endHour: Int, + val endMinute: Int, + ) + + data class TBR( + val rate: Double?, + val percentage: Int?, + val isAbsolute: Boolean, + val durationMinutes: Int, + val elapsedMinutes: Int, + ) + + data class InProgressBolus( + var requestedDose: Double = 0.0, + var currentDose: Double = 0.0, + var temporaryId: Long = 0, + var cancelled: Boolean = false, + var detailedBolusInfo: DetailedBolusInfo, + var treatment: EventOverviewBolusProgress.Treatment, + ) + + enum class Update { + AlarmsChanged, + BasalChanged, + TBRChanged, + ConstraintsChanged, + UnadvisedSettingsChanged, + BatteryChanged, + ReservoirChanged, + } + + data class PumpStatus( + val dateTime: DateTime, + val batteryLevel: BatteryLevel, + val reservoirLevel: Double, + val alarms: List, + val isAdvancedBolusEnabled: Boolean, + val currentBasalPattern: Int, + val maxBasal: Double, + val maxBolus: Double, + val tbr: TBR?, + val basal: Basal?, + ) { + fun overall(): String { + return "Date ${dateTime}, battery ${batteryLevel.percentage}, " + + "reservoir ${reservoirLevel}, alarms ${alarms.joinToString(", ", "[", "]") { it.name }}, " + + "maxBasal $maxBasal, maxBolus $maxBolus, " + + "TBR ${tbr?.rate}, basal ${basal?.rate}" + } + + fun getPumpStatus(rh: ResourceHelper): String = when { + alarms.isNotEmpty() -> rh.gs(R.string.overview_pump_status_alarm) + basal == null -> rh.gs(R.string.overview_pump_status_suspended) + else -> rh.gs(R.string.overview_pump_status_normal) + } + + fun getBatteryLevel(rh: ResourceHelper): String = if (batteryLevel.approximate) + rh.gs(R.string.overview_pump_battery_approximate, batteryLevel.percentage) + else + rh.gs(R.string.overview_pump_battery_exact, batteryLevel.percentage, batteryLevel.voltage) + + fun getReservoirLevel(rh: ResourceHelper): String = rh.gs(R.string.overview_pump_reservoir, reservoirLevel) + + fun getTBR(rh: ResourceHelper): String = if (tbr != null) { + val diff = tbr.durationMinutes - tbr.elapsedMinutes + val id = if (tbr.isAbsolute) R.string.overview_pump_tempbasal else R.string.overview_pump_tempbasal_percentage + val value = if (tbr.isAbsolute) tbr.rate else tbr.percentage + if (diff >= 60) + rh.gs(id, value, diff / 60, diff % 60) + else + rh.gs(id, value, diff) + } else "-" + + fun getBasal(rh: ResourceHelper): String = if (basal != null) { + rh.gs(R.string.overview_pump_basal, basal.rate, basal.endHour, basal.endMinute) + } else "-" + } + + data class StatusUpdate( + val changes: List, + val previous: PumpStatus?, + val current: PumpStatus, + ) +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt new file mode 100644 index 000000000000..fa8a9e7bc669 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -0,0 +1,499 @@ +package app.aaps.pump.apex + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceManager +import androidx.preference.PreferenceScreen +import app.aaps.core.data.plugin.PluginType +import app.aaps.core.data.pump.defs.ManufacturerType +import app.aaps.core.data.pump.defs.PumpDescription +import app.aaps.core.data.pump.defs.PumpType +import app.aaps.core.data.pump.defs.TimeChangeType +import app.aaps.core.interfaces.constraints.ConstraintsChecker +import app.aaps.core.interfaces.constraints.PluginConstraints +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.objects.Instantiator +import app.aaps.core.interfaces.plugin.PluginDescription +import app.aaps.core.interfaces.profile.Profile +import app.aaps.core.interfaces.pump.DetailedBolusInfo +import app.aaps.core.interfaces.pump.Pump +import app.aaps.core.interfaces.pump.PumpEnactResult +import app.aaps.core.interfaces.pump.PumpPluginBase +import app.aaps.core.interfaces.pump.PumpSync +import app.aaps.core.interfaces.pump.defs.fillFor +import app.aaps.core.interfaces.queue.CommandQueue +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.AapsSchedulers +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.EventAppExit +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.interfaces.utils.fabric.FabricPrivacy +import app.aaps.core.keys.Preferences +import app.aaps.core.objects.constraints.ConstraintObject +import app.aaps.core.validators.preferences.AdaptiveDoublePreference +import app.aaps.core.validators.preferences.AdaptiveListPreference +import app.aaps.core.validators.preferences.AdaptiveStringPreference +import app.aaps.pump.apex.connectivity.ApexBluetooth +import app.aaps.pump.apex.connectivity.commands.pump.AlarmLength +import app.aaps.pump.apex.connectivity.commands.pump.BolusEntry +import app.aaps.pump.apex.connectivity.commands.pump.Version +import app.aaps.pump.apex.ui.ApexFragment +import app.aaps.pump.apex.utils.keys.ApexBooleanKey +import app.aaps.pump.apex.utils.keys.ApexDoubleKey +import app.aaps.pump.apex.utils.keys.ApexStringKey +import app.aaps.pump.apex.utils.toApexReadableProfile +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking +import org.json.JSONException + +import org.json.JSONObject +import javax.inject.Inject + +/** + * @author Roman Rikhter (teledurak@gmail.com) + */ +class ApexPumpPlugin @Inject constructor( + aapsLogger: AAPSLogger, + rh: ResourceHelper, + commandQueue: CommandQueue, + val context: Context, + val preferences: Preferences, + val rxBus: RxBus, + val aapsSchedulers: AapsSchedulers, + val fabricPrivacy: FabricPrivacy, + val instantiator: Instantiator, + val dateUtil: DateUtil, + val pump: ApexPump, + private val constraintsChecker: ConstraintsChecker +): PumpPluginBase( + PluginDescription() + .mainType(PluginType.PUMP) + .fragmentClass(ApexFragment::class.java.name) + .pluginIcon(R.drawable.ic_apex) + .pluginName(R.string.apex_plugin_name) + .shortName(R.string.apex_plugin_shortname) + .description(R.string.apex_plugin_description) + .preferencesId(PluginDescription.PREFERENCE_SCREEN), + aapsLogger, rh, commandQueue +), Pump, PluginConstraints { + + init { + preferences.registerPreferences(ApexBooleanKey::class.java) + preferences.registerPreferences(ApexDoubleKey::class.java) + preferences.registerPreferences(ApexStringKey::class.java) + } + + private val disposable = CompositeDisposable() + + private var service: ApexService? = null + private val connection: ServiceConnection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName) { + aapsLogger.debug(LTag.PUMP, "Service is disconnected") + service = null + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + aapsLogger.debug(LTag.PUMP, "Service is connected") + val mLocalBinder = service as ApexService.LocalBinder + this@ApexPumpPlugin.service = mLocalBinder.serviceInstance.also { + it.startConnection() + } + } + } + + + override fun onStart() { + super.onStart() + aapsLogger.debug(LTag.PUMP, "Starting APEX plugin") + context.bindService(Intent(context, ApexService::class.java), connection, Context.BIND_AUTO_CREATE) + disposable += rxBus + .toObservable(EventAppExit::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ context.unbindService(connection) }, fabricPrivacy::logException) + } + + override fun onStop() { + aapsLogger.debug(LTag.PUMP, "Stopping APEX plugin") + service?.disconnect() + context.unbindService(connection) + disposable.clear() + super.onStop() + } + + override val isFakingTempsByExtendedBoluses = false + override fun isBusy() = false + override fun canHandleDST() = false + + override fun manufacturer() = ManufacturerType.Apex + override fun model() = PumpType.APEX_TRUCARE_III + override fun serialNumber() = preferences.get(ApexStringKey.LastConnectedSerialNumber) + + override val baseBasalRate: Double + get() = pump.basal?.rate ?: 0.0 + override val reservoirLevel: Double + get() = pump.reservoirLevel + override val batteryLevel: Int + get() = pump.batteryLevel.percentage + override val pumpDescription = PumpDescription().fillFor(model()) + + override fun isSuspended() = pump.isSuspended + override fun isInitialized() = !pump.gettingReady && pump.status != null + override fun isConnecting() = service?.connectionStatus == ApexBluetooth.Status.CONNECTING + override fun isHandshakeInProgress() = false + override fun isConnected() = service?.connectionStatus == ApexBluetooth.Status.CONNECTED + override fun isBatteryChangeLoggingEnabled() = preferences.get(ApexBooleanKey.LogBatteryChange) + + // We should be always connected to the pump. + override fun connect(reason: String) { + aapsLogger.debug(LTag.PUMP, "Triggered connect: $reason") + if (service?.apexBluetooth?.status == ApexBluetooth.Status.DISCONNECTED) { + service?.startConnection() + } + } + override fun disconnect(reason: String) { + aapsLogger.debug(LTag.PUMP, "Triggered disconnect: $reason") + } + override fun stopConnecting() { + aapsLogger.debug(LTag.PUMP, "Triggered stopConnecting") + } + + override fun getJSONStatus(profile: Profile, profileName: String, version: String): JSONObject { + val now = System.currentTimeMillis() + if (!isInitialized()) return JSONObject() + + val date = pump.dateTime + if (date.millis + 60 * 60 * 1000L < System.currentTimeMillis()) { + return JSONObject() + } + val status = pump.status!! + + val pumpJson = JSONObject() + val statusJson = JSONObject() + val extendedJson = JSONObject() + try { + statusJson.put( + "status", if (!isSuspended()) "normal" + else if (isInitialized() && isSuspended()) "suspended" + else "inactive" + ) + statusJson.put("timestamp", dateUtil.toISOString(date.millis)) + val lastBolus = pump.lastBolus + if (lastBolus != null) { + extendedJson.put("lastBolus", dateUtil.dateAndTimeString(lastBolus.dateTime.millis)) + extendedJson.put("lastBolusAmount", lastBolus.standardPerformed * 0.025) + } + if (status.tbr != null) { + extendedJson.put("TempBasalAbsoluteRate", status.tbr.rate ?: (baseBasalRate * status.tbr.percentage!! / 100.0)) + extendedJson.put("TempBasalStart", dateUtil.dateAndTimeString(now - status.tbr.elapsedMinutes * 1000)) + extendedJson.put("TempBasalRemaining", status.tbr.elapsedMinutes - status.tbr.durationMinutes) + } + extendedJson.put("BaseBasalRate", baseBasalRate) + try { + extendedJson.put("ActiveProfile", profileName) + } catch (ignored: Exception) {} + pumpJson.put("status", statusJson) + pumpJson.put("extended", extendedJson) + pumpJson.put("reservoir", status.reservoirLevel.toInt()) + pumpJson.put("clock", dateUtil.toISOString(now)) + } catch (e: JSONException) { + aapsLogger.error(LTag.PUMP, "Unhandled exception: $e") + } + return pumpJson + } + + override fun shortStatus(veryShort: Boolean): String { + if (!isInitialized()) return rh.gs(app.aaps.pump.common.R.string.pump_status_not_initialized) + val status = pump.status!! + + val ret = "${rh.gs(R.string.status_conn_status)}: ${service!!.connectionStatus.toLocalString(rh)}\n" + + "${rh.gs(R.string.status_pump_status)}: ${status.getPumpStatus(rh)}\n" + + "${rh.gs(R.string.status_last_bolus)}: ${pump.lastBolus?.toShortLocalString(rh) ?: "-"}\n" + + "${rh.gs(R.string.status_tbr)}: ${status.getTBR(rh)}\n" + + "${rh.gs(R.string.status_basal)}: ${status.getBasal(rh)}\n" + + "${rh.gs(R.string.status_battery)}: ${status.getBatteryLevel(rh)}" + + aapsLogger.debug(LTag.PUMP, "Short status: $ret") + return ret + } + + fun updatePumpDescription() { + aapsLogger.debug(LTag.PUMP, "Updating pump description") + pumpDescription.maxTempAbsolute = pump.maxBasal + pumpDescription.basalMaximumRate = pump.maxBasal + } + + override fun loadTDDs(): PumpEnactResult { + val ret = instantiator.providePumpEnactResult() + if (!isInitialized()) { + return ret.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_not_ready) + } + } + + val status = service!!.getTDDs("ApexPumpPlugin-loadTDDs") + return ret.apply { + success = status + enacted = status + } + } + + override fun getPumpStatus(reason: String) { + if (!isInitialized()) return + aapsLogger.debug(LTag.PUMP, "Requested pump status cause of $reason") + if (!service!!.getStatus("ApexPumpPlugin-getPumpStatus")) return + } + + override fun setNewBasalProfile(profile: Profile): PumpEnactResult { + val ret = instantiator.providePumpEnactResult() + if (!isInitialized()) { + return ret.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_not_ready) + } + } + + var result = service!!.updateBasalPatternIndex(ApexService.USED_BASAL_PATTERN_INDEX, "ApexPumpPlugin-setNewBasalProfile") + if (!result) { + return ret.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_failed_to_switch_basal_profile_index) + } + } + + result = service!!.updateCurrentBasalPattern(profile.toApexReadableProfile(), "ApexPumpPlugin-setNewBasalProfile") + if (!result) { + return ret.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_failed_to_update_basal_profile) + } + } + + return ret.apply { + success = true + enacted = true + } + } + + override fun isThisProfileSet(profile: Profile): Boolean { + if (!isInitialized()) return false + val profileBasal = profile.toApexReadableProfile() + val pumpBasals = service!!.getBasalProfiles("ApexPumpPlugin-isThisProfileSet") ?: return false + val pumpBasal = pumpBasals[ApexService.USED_BASAL_PATTERN_INDEX] + return pumpBasal == profileBasal + } + + override fun lastDataTime(): Long { + if (service == null) return System.currentTimeMillis() + return service!!.lastConnected + } + + override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { + // Insulin value must be greater than 0 + require(detailedBolusInfo.carbs == 0.0) { detailedBolusInfo.toString() } + require(detailedBolusInfo.insulin > 0) { detailedBolusInfo.toString() } + val pumpEnactResult = instantiator.providePumpEnactResult() + detailedBolusInfo.insulin = constraintsChecker + .applyBolusConstraints(ConstraintObject(detailedBolusInfo.insulin, aapsLogger)) + .value() + + if (!isInitialized()) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_not_ready) + } + } + + if (isSuspended()) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_pump_suspended) + } + } + + val result = service!!.bolus(detailedBolusInfo, "ApexPumpPlugin-deliverTreatment") + if (!result) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_bolus_start_failed) + } + } + + return runBlocking { + val bolus = service!!.bolusCompletable?.await() + pumpEnactResult.apply { + success = bolus != null + if (bolus != null) { + enacted = bolus.currentDose >= 0.025 + bolusDelivered = bolus.currentDose + } + } + } + } + + override fun stopBolusDelivering() { + if (!isInitialized()) return + service!!.cancelBolus("ApexPumpPlugin-stopBolusDelivering") + } + + override fun setTempBasalAbsolute(absoluteRate: Double, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { + val pumpEnactResult = instantiator.providePumpEnactResult() + val rate = constraintsChecker + .applyBasalConstraints(ConstraintObject(absoluteRate, aapsLogger), profile) + .value() + val duration = durationInMinutes - durationInMinutes % 15 + + if (!isInitialized()) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_not_ready) + } + } + + if (isSuspended()) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_pump_suspended) + } + } + + val status = pump.status!! + if (enforceNew && status.tbr != null) { + val result = service!!.cancelTemporaryBasal("ApexPumpPlugin-setTempBasal") + if (!result) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_tbr_cancel_failed) + } + } + } + + val result = service!!.temporaryBasal(rate, duration, tbrType, "ApexPumpPlugin-setTempBasal") + if (!result) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_tbr_set_failed) + } + } + + return pumpEnactResult.apply { + success = true + enacted = true + } + } + + override fun setTempBasalPercent(percent: Int, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { + return instantiator.providePumpEnactResult().apply { + success = false + enacted = false + comment = rh.gs(R.string.error_only_absolute_supported) + } + } + + override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult { + val pumpEnactResult = instantiator.providePumpEnactResult() + if (!isInitialized()) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_not_ready) + } + } + + if (isSuspended()) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_pump_suspended) + } + } + + val status = service!!.cancelTemporaryBasal("ApexPumpPlugin-cancelTempBasal") + if (!status) { + return pumpEnactResult.apply { + success = false + enacted = false + comment = rh.gs(R.string.error_tbr_cancel_failed) + } + } + + return pumpEnactResult.apply { + success = true + enacted = true + } + } + + override fun setExtendedBolus(insulin: Double, durationInMinutes: Int): PumpEnactResult { + // Not yet supported + return instantiator.providePumpEnactResult().apply { + success = false + enacted = false + comment = rh.gs(R.string.error_not_ready) + } + } + + override fun cancelExtendedBolus(): PumpEnactResult { + // Not yet supported + return instantiator.providePumpEnactResult().apply { + success = false + enacted = false + comment = rh.gs(R.string.error_not_ready) + } + } + + override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) { + if (!isInitialized()) return + service!!.syncDateTime("ApexService-timezoneOrDSTChanged") + } + + override fun addPreferenceScreen(preferenceManager: PreferenceManager, parent: PreferenceScreen, context: Context, requiredKey: String?) { + if (requiredKey != null) return + val category = PreferenceCategory(context) + parent.addPreference(category) + category.apply { + key = "apex_settings" + title = rh.gs(R.string.apex_settings) + initialExpandedChildrenCount = 0 + addPreference(AdaptiveStringPreference( + ctx = context, + stringKey = ApexStringKey.SerialNumber, + title = R.string.setting_serial_number, + )) + addPreference(AdaptiveListPreference( + ctx = context, + stringKey = ApexStringKey.AlarmSoundLength, + title = R.string.setting_alarm_length, + entries = arrayOf(rh.gs(R.string.setting_alarm_length_long), rh.gs(R.string.setting_alarm_length_medium), rh.gs(R.string.setting_alarm_length_short)), + entryValues = arrayOf(AlarmLength.Long.name, AlarmLength.Medium.name, AlarmLength.Short.name) + )) + addPreference(AdaptiveDoublePreference( + ctx = context, + doubleKey = ApexDoubleKey.MaxBasal, + title = R.string.setting_max_basal, + )) + addPreference(AdaptiveDoublePreference( + ctx = context, + doubleKey = ApexDoubleKey.MaxBolus, + title = R.string.setting_max_bolus, + )) + } + } +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt new file mode 100644 index 000000000000..7ee550155efa --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -0,0 +1,1039 @@ +package app.aaps.pump.apex + +import app.aaps.pump.apex.interfaces.ApexBluetoothCallback +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import android.os.SystemClock +import app.aaps.core.data.model.BS +import app.aaps.core.data.model.TE +import app.aaps.core.data.pump.defs.PumpType +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.Notification +import app.aaps.core.interfaces.pump.DetailedBolusInfo +import app.aaps.core.interfaces.pump.PumpSync +import app.aaps.core.interfaces.queue.CommandQueue +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.AapsSchedulers +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.EventDismissNotification +import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress +import app.aaps.core.interfaces.rx.events.EventPreferenceChange +import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged +import app.aaps.core.interfaces.ui.UiInteraction +import app.aaps.core.interfaces.utils.fabric.FabricPrivacy +import app.aaps.core.keys.Preferences +import app.aaps.core.utils.notifyAll +import app.aaps.core.utils.waitMillis +import app.aaps.pump.apex.connectivity.ApexBluetooth +import app.aaps.pump.apex.connectivity.ProtocolVersion +import app.aaps.pump.apex.connectivity.commands.device.Bolus +import app.aaps.pump.apex.connectivity.commands.device.CancelBolus +import app.aaps.pump.apex.connectivity.commands.device.CancelTemporaryBasal +import app.aaps.pump.apex.connectivity.commands.device.DeviceCommand +import app.aaps.pump.apex.connectivity.commands.device.ExtendedBolus +import app.aaps.pump.apex.connectivity.commands.device.GetValue +import app.aaps.pump.apex.connectivity.commands.device.NotifyAboutConnection +import app.aaps.pump.apex.connectivity.commands.device.SyncDateTime +import app.aaps.pump.apex.connectivity.commands.device.TemporaryBasal +import app.aaps.pump.apex.connectivity.commands.device.UpdateBasalProfileRates +import app.aaps.pump.apex.connectivity.commands.device.UpdateSystemState +import app.aaps.pump.apex.connectivity.commands.device.UpdateUsedBasalProfile +import app.aaps.pump.apex.connectivity.commands.pump.Alarm +import app.aaps.pump.apex.connectivity.commands.pump.AlarmLength +import app.aaps.pump.apex.connectivity.commands.pump.AlarmObject +import app.aaps.pump.apex.connectivity.commands.pump.BasalProfile +import app.aaps.pump.apex.connectivity.commands.pump.BolusEntry +import app.aaps.pump.apex.connectivity.commands.pump.CommandResponse +import app.aaps.pump.apex.connectivity.commands.pump.Heartbeat +import app.aaps.pump.apex.connectivity.commands.pump.PumpCommand +import app.aaps.pump.apex.connectivity.commands.pump.PumpObject +import app.aaps.pump.apex.connectivity.commands.pump.PumpObjectModel +import app.aaps.pump.apex.connectivity.commands.pump.StatusV1 +import app.aaps.pump.apex.connectivity.commands.pump.TDDEntry +import app.aaps.pump.apex.connectivity.commands.pump.Version +import app.aaps.pump.apex.events.EventApexPumpDataChanged +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.keys.ApexBooleanKey +import app.aaps.pump.apex.utils.keys.ApexDoubleKey +import app.aaps.pump.apex.utils.keys.ApexStringKey +import dagger.android.DaggerService +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.sync.Mutex +import org.joda.time.DateTime +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject +import kotlin.concurrent.schedule +import kotlin.math.abs + +/** + * @author Roman Rikhter (teledurak@gmail.com) + */ +class ApexService: DaggerService(), ApexBluetoothCallback { + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var aapsSchedulers: AapsSchedulers + @Inject lateinit var preferences: Preferences + @Inject lateinit var rxBus: RxBus + @Inject lateinit var apexBluetooth: ApexBluetooth + @Inject lateinit var apexDeviceInfo: ApexDeviceInfo + @Inject lateinit var apexPumpPlugin: ApexPumpPlugin + @Inject lateinit var uiInteraction: UiInteraction + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var commandQueue: CommandQueue + @Inject lateinit var pumpSync: PumpSync + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var pump: ApexPump + + companion object { + const val USED_BASAL_PATTERN_INDEX = 7 + val FIRST_SUPPORTED_PROTO = ProtocolVersion.PROTO_4_10 + val LAST_SUPPORTED_PROTO = ProtocolVersion.PROTO_4_10 + } + + private data class InCommandResponse( + var response: CommandResponse? = null, + var waiting: Boolean = false, + ) { + fun clear(notify: Boolean = false) { + response = null + waiting = true + if (notify) this.notifyAll() + } + } + + private data class InGetValueResult( + var isSingleObject: Boolean = false, + var targetObject: PumpObject? = null, + var waiting: Boolean = false, + var response: ArrayList? = null, + ) { + fun add(data: PumpObjectModel) { + if (response == null) response = arrayListOf() + response!!.add(data) + } + + fun clear(notify: Boolean = false) { + response = null + targetObject = null + isSingleObject = false + waiting = true + if (notify) this.notifyAll() + } + } + + private val commandLock = Mutex() + private val disposable = CompositeDisposable() + + private val getValueResult = InGetValueResult() + private val commandResponse = InCommandResponse() + + private var timer = Timer("ApexService-timer") + + private var getValueLastTaskTimestamp: Long = 0 + private var unreachableTimerTask: TimerTask? = null + + private var waitingForCurrentBolusInHistory = false + + private var _bolusCompletable: CompletableDeferred? = null + private var statusGetValue: GetValue.Value = GetValue.Value.StatusV1 + + private var lastBolusDateTime = DateTime(0) + private var lastConnectedTimestamp = System.currentTimeMillis() + + val lastConnected: Long + get() = if (connectionStatus != ApexBluetooth.Status.CONNECTED) { + lastConnectedTimestamp + } else System.currentTimeMillis() + + fun getValue(value: GetValue.Value): List? = synchronized(commandLock) { synchronized(getValueResult) { + if (connectionStatus != ApexBluetooth.Status.CONNECTED) return null + aapsLogger.debug(LTag.PUMPCOMM, "Executing GetValue(${value.name})") + + getValueResult.clear() + getValueResult.targetObject = when (value) { + GetValue.Value.StatusV1 -> PumpObject.StatusV1 + GetValue.Value.TDDs -> PumpObject.TDDEntry + GetValue.Value.Alarms -> PumpObject.AlarmEntry + GetValue.Value.BasalProfiles -> PumpObject.BasalProfile + GetValue.Value.Version -> PumpObject.FirmwareEntry + GetValue.Value.BolusHistory, GetValue.Value.LatestBoluses -> PumpObject.BolusEntry + GetValue.Value.LatestTemporaryBasals, GetValue.Value.StatusV2 -> return null // TODO 4.11 bring up + GetValue.Value.WizardStatus -> return null + } + getValueResult.isSingleObject = when (value) { + GetValue.Value.StatusV1, GetValue.Value.StatusV2, GetValue.Value.Version -> true + else -> false + } + + apexBluetooth.send(GetValue(apexDeviceInfo, value)) + try { + aapsLogger.debug(LTag.PUMPCOMM, "${value.name} | Waiting for response") + getValueResult.waitMillis(if (getValueResult.isSingleObject) 5000 else 60000) + } catch (e: InterruptedException) { + aapsLogger.error(LTag.PUMPCOMM, "getValue InterruptedException", e) + } + + getValueResult.response + }} + + private fun executeWithResponse(command: DeviceCommand): CommandResponse? = synchronized(commandLock) { synchronized(commandResponse) { + if (connectionStatus != ApexBluetooth.Status.CONNECTED) return null + + aapsLogger.debug(LTag.PUMPCOMM, "Executing $command") + commandResponse.clear() + + apexBluetooth.send(command) + try { + aapsLogger.debug(LTag.PUMPCOMM, "$command | Waiting for response") + commandResponse.waitMillis(5000) + } catch (e: InterruptedException) { + aapsLogger.error(LTag.PUMPCOMM, "executeWithResponse InterruptedException", e) + } + + commandResponse.response + }} + + override fun onCreate() { + super.onCreate() + aapsLogger.debug(LTag.PUMP, "Service created") + apexBluetooth.setCallback(this) + + disposable += rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ + if (it.isChanged(ApexStringKey.SerialNumber.key)) { + onSerialChanged() + } else if (it.isChanged(ApexDoubleKey.MaxBolus.key) || it.isChanged(ApexDoubleKey.MaxBasal.key) || it.isChanged(ApexStringKey.AlarmSoundLength.key) && apexBluetooth.status == ApexBluetooth.Status.CONNECTED) { + updateSettings("ApexService-PreferencesListener") + } + }, fabricPrivacy::logException) + + pump.serialNumber = apexDeviceInfo.serialNumber + } + + override fun onDestroy() { + aapsLogger.debug(LTag.PUMP, "Service destroyed") + disposable.clear() + disconnect() + super.onDestroy() + } + + private fun onSerialChanged() { + pump.serialNumber = apexDeviceInfo.serialNumber + if (apexBluetooth.status != ApexBluetooth.Status.DISCONNECTED) disconnect() + startConnection() + } + + //////// Public methods + + fun syncDateTime(caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "syncDateTime - $caller") + val response = executeWithResponse(SyncDateTime(apexDeviceInfo, DateTime.now())) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[syncDateTime caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to sync time: ${response.code.name}") + return false + } + + return true + } + + fun notifyAboutConnection(caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "notifyAboutConnection - $caller") + val response = executeWithResponse(NotifyAboutConnection(apexDeviceInfo)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[notifyAboutConnection caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to notify about connection: ${response.code.name}") + return false + } + + return true + } + + fun bolus(dbi: DetailedBolusInfo, caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "bolus - $caller") + if (dbi.insulin > pump.maxBolus) { + aapsLogger.error(LTag.PUMP, "[bolus caller=$caller] Requested ${dbi.insulin}U is greater than maximum set ${pump.maxBolus}") + return false + } + + val doseRaw = (dbi.insulin / 0.025).toInt() + if (dbi.insulin % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[bolus caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounding down.") + + val response = executeWithResponse(Bolus(apexDeviceInfo, doseRaw)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[bolus caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to begin bolus: ${response.code.name}") + return false + } + + val syncResult = pumpSync.addBolusWithTempId( + timestamp = dbi.timestamp, + amount = dbi.insulin, + temporaryId = dbi.timestamp, + type = dbi.bolusType, + pumpSerial = apexDeviceInfo.serialNumber, + pumpType = PumpType.APEX_TRUCARE_III, + ) + aapsLogger.debug(LTag.PUMP, "Initial bolus [${dbi.insulin}U] sync succeeded? $syncResult") + + pump.inProgressBolus = ApexPump.InProgressBolus( + requestedDose = dbi.insulin, + temporaryId = dbi.timestamp, + detailedBolusInfo = dbi, + treatment = EventOverviewBolusProgress.Treatment( + insulin = dbi.insulin, + carbs = dbi.carbs.toInt(), + isSMB = dbi.bolusType == BS.Type.SMB, + id = dbi.id + ) + ) + + _bolusCompletable = CompletableDeferred() + return true + } + + fun extendedBolus(dose: Double, durationMinutes: Int, caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "extendedBolus - $caller") + val doseRaw = (dose / 0.025).toInt() + if (dose % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounded down.") + + val durationRaw = durationMinutes / 15 + if (durationMinutes % 15 > 0) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus duration is not aligned to 15 minute steps! Rounded down.") + + val response = executeWithResponse(ExtendedBolus(apexDeviceInfo, doseRaw, durationRaw)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to begin extended bolus: ${response.code.name}") + return false + } + + return true + } + + fun temporaryBasal(dose: Double, durationMinutes: Int, type: PumpSync.TemporaryBasalType? = null, caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "temporaryBasal - $caller") + if (dose > pump.maxBasal) { + aapsLogger.error(LTag.PUMP, "[temporaryBasal caller=$caller] Requested ${dose}U is greater than maximum set ${pump.maxBasal}U") + return false + } + + val doseRaw = (dose / 0.025).toInt() + if (dose % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounded down.") + + val durationRaw = durationMinutes / 15 + if (durationMinutes % 15 > 0) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus duration is not aligned to 15 minute steps! Rounded down.") + + val response = executeWithResponse(TemporaryBasal(apexDeviceInfo, true, doseRaw, durationRaw)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to begin extended bolus: ${response.code.name}") + return false + } + + val id = System.currentTimeMillis() + pumpSync.syncTemporaryBasalWithPumpId( + timestamp = id, + pumpId = id, + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + rate = dose, + duration = durationMinutes.toLong() * 60 * 1000, + isAbsolute = true, + type = type, + ) + + aapsLogger.debug(LTag.PUMP, "Started TBR ${dose}U for ${durationMinutes}min by $caller") + return true + } + + fun cancelBolus(caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "cancelBolus - $caller") + val response = executeWithResponse(CancelBolus(apexDeviceInfo)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[cancelBolus caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to cancel bolus: ${response.code.name}") + return false + } + + onBolusFailed(true) + return true + } + + fun cancelTemporaryBasal(caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "cancelTemporaryBasal - $caller") + val response = executeWithResponse(CancelTemporaryBasal(apexDeviceInfo)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[cancelTemporaryBasal caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to cancel temporary basal: ${response.code.name}") + return false + } + + val stop = System.currentTimeMillis() + pumpSync.syncStopTemporaryBasalWithPumpId( + timestamp = stop, + endPumpId = stop, + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + ) + + return true + } + + fun updateSettings(caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "updateSettings - $caller") + val response = executeWithResponse( + if (pump.isV1) + pump.lastV1!!.toUpdateSettingsV1( + apexDeviceInfo, + AlarmLength.valueOf(preferences.get(ApexStringKey.AlarmSoundLength)), + maxSingleBolus = (preferences.get(ApexDoubleKey.MaxBolus) / 0.025).toInt(), + maxBasalRate = (preferences.get(ApexDoubleKey.MaxBasal) / 0.025).toInt(), + enableAdvancedBolus = false, + ) + else + return false + ) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[updateSettings caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to update settings: ${response.code.name}") + return false + } + + return true + } + + fun updateSystemState(suspend: Boolean, caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "updateSystemState - $caller") + val response = executeWithResponse(UpdateSystemState(apexDeviceInfo, suspend)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[updateSystemState caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to update system state: ${response.code.name}") + return false + } + + return true + } + + fun updateBasalPatternIndex(id: Int, caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "updateBasalPatternIndex - $caller") + val response = executeWithResponse(UpdateUsedBasalProfile(apexDeviceInfo, id)) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[updateBasalPatternIndex caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to update basal pattern index: ${response.code.name}") + return false + } + + return true + } + + fun updateCurrentBasalPattern(doses: List, caller: String): Boolean { + require(doses.size == 48) + + aapsLogger.debug(LTag.PUMPCOMM, "updateCurrentBasalPattern - $caller") + + val response = executeWithResponse(UpdateBasalProfileRates( + apexDeviceInfo, + doses.map { (it / 0.025).toInt() } + )) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[updateBasalPatternIndex caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + if (response.code != CommandResponse.Code.Accepted) { + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to update basal pattern index: ${response.code.name}") + return false + } + + return true + } + + fun getTDDs(caller: String): Boolean { + aapsLogger.debug(LTag.PUMPCOMM, "getTDDs - $caller") + val response = getValue(GetValue.Value.TDDs) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[getTDDs caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + return true + } + + fun getBoluses(caller: String, isFullHistory: Boolean = false): Boolean { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.getting_boluses))) + + aapsLogger.debug(LTag.PUMPCOMM, "getBoluses - $caller") + val response = getValue(if (isFullHistory) GetValue.Value.BolusHistory else GetValue.Value.LatestBoluses) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[getBoluses full=$isFullHistory caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + return true + } + + fun getStatus(caller: String): Boolean { + rxBus.send(EventPumpStatusChanged(rh.gs(R.string.getting_pump_status))) + aapsLogger.debug(LTag.PUMPCOMM, "getStatus - $caller") + val response = getValue(statusGetValue) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[getStatus caller=$caller] Timed out while trying to communicate with the pump") + return false + } + + return true + } + + fun getBasalProfiles(caller: String): Map>? { + val ret = mutableMapOf>() + + aapsLogger.debug(LTag.PUMPCOMM, "getBasalProfiles - $caller") + + val response = getValue(GetValue.Value.BasalProfiles) + if (response == null) { + aapsLogger.error(LTag.PUMPCOMM, "[getBasalProfiles caller=$caller] Timed out while trying to communicate with the pump") + return null + } + + for (i in response) { + require(i is BasalProfile) + ret[i.index] = i.rates.map { it * 0.025 } + } + + return ret + } + + //////// Public values + + val connectionStatus: ApexBluetooth.Status + get() = apexBluetooth.status + + val bolusCompletable: CompletableDeferred? + get() = _bolusCompletable + + //////// Pump commands handlers + + private fun onBolusProgress(dose: Double) { + aapsLogger.debug(LTag.PUMPCOMM, "bolus progress $dose") + pump.inProgressBolus?.currentDose = dose + + val bolus = pump.inProgressBolus ?: return + + if (bolus.detailedBolusInfo.bolusType == BS.Type.SMB) { + rxBus.send(EventPumpStatusChanged(rh.gs(app.aaps.core.ui.R.string.smb_bolus_u, bolus.requestedDose))) + } else { + rxBus.send(EventPumpStatusChanged(rh.gs(app.aaps.core.ui.R.string.bolus_u_min, bolus.requestedDose))) + } + + rxBus.send(EventOverviewBolusProgress.apply { + t = bolus.treatment + percent = (bolus.currentDose / bolus.requestedDose * 100).toInt() + status = rh.gs(R.string.status_delivering, dose) + }) + } + + private fun onBolusCompleted(dose: Double) { + aapsLogger.debug(LTag.PUMPCOMM, "bolus completed") + if (pump.inProgressBolus == null) return + pump.inProgressBolus!!.currentDose = dose + + rxBus.send(EventOverviewBolusProgress.apply { + percent = 100 + status = rh.gs(R.string.status_delivered, dose) + }) + + // Request new bolus history to fixup bolus ID. + getBoluses("ApexService-onBolusCompleted") + } + + private fun onBolusFailed(cancelled: Boolean = false) { + aapsLogger.debug(LTag.PUMPCOMM, "bolus failed (cancelled? $cancelled)") + if (pump.inProgressBolus == null) return + + if (cancelled) { + pump.inProgressBolus!!.cancelled = true + rxBus.send(EventOverviewBolusProgress.apply { + status = rh.gs(R.string.status_bolus_cancelled) + }) + } + + if (pump.inProgressBolus!!.currentDose >= 0.025) { + // Request new bolus history to fixup bolus ID and delivered amount. + getBoluses("ApexService-onBolusCompleted") + } else { + aapsLogger.debug(LTag.PUMPCOMM, "bolus entirely failed!") + // TODO: how to handle fully failed boluses? + pump.inProgressBolus = null + _bolusCompletable?.complete(null) + } + } + + private fun onCommandResponse(response: CommandResponse) { + aapsLogger.debug(LTag.PUMPCOMM, "got command response - ${response.code.name} / ${response.dose}") + when (response.code) { + CommandResponse.Code.Accepted, CommandResponse.Code.Invalid -> { + if (!commandResponse.waiting) return + commandResponse.response = response + commandResponse.waiting = false + synchronized(commandResponse) { + commandResponse.notifyAll() + } + } + CommandResponse.Code.StandardBolusProgress -> onBolusProgress(response.dose * 0.025) + CommandResponse.Code.ExtendedBolusProgress -> return + CommandResponse.Code.Completed -> onBolusCompleted(response.dose * 0.025) + else -> return + } + } + + private fun onAlarmsChanged(update: ApexPump.StatusUpdate) { + // Alarm was dismissed + if (pump.isAlarmPresent && update.current.alarms.isEmpty()) { + pump.isAlarmPresent = false + rxBus.send(EventDismissNotification(Notification.PUMP_ERROR)) + rxBus.send(EventDismissNotification(Notification.PUMP_WARNING)) + } + + // New alarms + if (!pump.isAlarmPresent && update.current.alarms.isNotEmpty()) { + if (pump.isBolusing) { + // Pump sends early heartbeat while bolusing if there's an error while bolusing. + aapsLogger.error(LTag.PUMP, "Bolus has failed!") + onBolusFailed() + } + + for (alarm in update.current.alarms) { + val name = when (alarm) { + Alarm.NoDosage, Alarm.NoDelivery -> rh.gs(R.string.alarm_occlusion) + Alarm.NoReservoir -> rh.gs(R.string.alarm_reservoir_empty) + Alarm.DeadBattery -> rh.gs(R.string.alarm_battery_dead) + Alarm.LowBattery -> rh.gs(R.string.alarm_w_battery_low) + Alarm.LowReservoir -> rh.gs(R.string.alarm_w_reservoir_low) + Alarm.EncoderError, Alarm.FRAMError, Alarm.ClockError, Alarm.TimeError, + Alarm.TimeAnomalyError, Alarm.MotorAbnormal, Alarm.MotorPowerAbnormal, + Alarm.MotorError -> rh.gs(R.string.alarm_hardware_fault, alarm.name) + Alarm.Unknown -> rh.gs(R.string.alarm_unknown_error) + else -> rh.gs(R.string.alarm_unknown_error_name, alarm.name) + } + val isUrgent = when(alarm) { + Alarm.LowBattery, Alarm.LowReservoir -> false + else -> true + } + + uiInteraction.addNotification( + if (isUrgent) Notification.PUMP_ERROR else Notification.PUMP_WARNING, + name, + if (isUrgent) Notification.URGENT else Notification.NORMAL, + ) + pumpSync.insertAnnouncement( + error = name, + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + ) + } + } + } + + private fun onBasalChanged(update: ApexPump.StatusUpdate) { + if (update.current.basal == null) { + uiInteraction.addNotification( + Notification.PUMP_SUSPENDED, + rh.gs(R.string.notification_pump_is_suspended), + if (pump.isBolusing) Notification.URGENT else Notification.NORMAL, + ) + commandQueue.loadEvents(null) + return + } else { + rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED)) + } + } + + private fun onSettingsChanged(update: ApexPump.StatusUpdate) { + if (pump.settingsAreUnadvised && preferences.get(ApexDoubleKey.MaxBasal) != 0.0 && preferences.get(ApexDoubleKey.MaxBolus) != 0.0) updateSettings("ApexService-onSettingsChanged") + if (update.current.currentBasalPattern != USED_BASAL_PATTERN_INDEX) updateBasalPatternIndex(USED_BASAL_PATTERN_INDEX, "ApexService-onSettingsChanged") + } + + private fun onBatteryChanged(update: ApexPump.StatusUpdate) { + val cur = update.current + update.previous?.let { old -> + // Percentage became higher - battery was changed. + if (cur.batteryLevel.percentage - 2 > old.batteryLevel.percentage && preferences.get(ApexBooleanKey.LogBatteryChange)) { + pumpSync.insertTherapyEventIfNewWithTimestamp( + timestamp = System.currentTimeMillis(), + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + type = TE.Type.PUMP_BATTERY_CHANGE, + ) + aapsLogger.debug(LTag.PUMP, "Logged battery change") + } + } + } + + private fun onReservoirChanged(update: ApexPump.StatusUpdate) { + val cur = update.current + update.previous?.let { old -> + // Reservoir level became higher - insulin was changed. + if (cur.reservoirLevel - 2 > old.reservoirLevel && preferences.get(ApexBooleanKey.LogInsulinChange)) { + pumpSync.insertTherapyEventIfNewWithTimestamp( + timestamp = System.currentTimeMillis(), + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + type = TE.Type.INSULIN_CHANGE, + ) + aapsLogger.debug(LTag.PUMP, "Logged insulin change") + } + } + } + + private fun onTBRChanged(update: ApexPump.StatusUpdate) { + // if (update.current.tbr == null && update.previous?.tbr != null) { + // val stop = System.currentTimeMillis() + // pumpSync.syncStopTemporaryBasalWithPumpId( + // timestamp = stop, + // endPumpId = stop, + // pumpType = PumpType.APEX_TRUCARE_III, + // pumpSerial = apexDeviceInfo.serialNumber, + // ) + // aapsLogger.debug(LTag.PUMP, "Detected TBR cancellation") + // } + } + + private fun onConstraintsChanged(update: ApexPump.StatusUpdate) { + preferences.put(ApexDoubleKey.MaxBasal, update.current.maxBasal) + preferences.put(ApexDoubleKey.MaxBolus, update.current.maxBolus) + } + + private fun onStatusCommon(update: ApexPump.StatusUpdate) { + aapsLogger.debug(LTag.PUMPCOMM, "Status updates: ${update.changes.joinToString(", ") { it.name }}") + + preferences.put(ApexDoubleKey.MaxBasal, update.current.maxBasal) + preferences.put(ApexDoubleKey.MaxBolus, update.current.maxBolus) + apexPumpPlugin.updatePumpDescription() + + onAlarmsChanged(update) + onBasalChanged(update) + onSettingsChanged(update) + onBatteryChanged(update) + onReservoirChanged(update) + onTBRChanged(update) + onConstraintsChanged(update) + } + + private fun onStatusV1(status: StatusV1) { + aapsLogger.debug(LTag.PUMPCOMM, "Got status V1") + val updates = pump.updateFromV1(status) + onStatusCommon(updates) + } + + private fun onHeartbeat() { + aapsLogger.debug(LTag.PUMPCOMM, "Got heartbeat") + if (pump.gettingReady) return + + if (!getStatus("HeartbeatHandler")) return + if (!getBoluses("HeartbeatHandler")) return + + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) + } + + private fun onVersion(version: Version) { + aapsLogger.debug(LTag.PUMPCOMM, "Got version") + if (version.atleastProto(ProtocolVersion.PROTO_4_11)) { + statusGetValue = GetValue.Value.StatusV2 + } + } + + private fun onBolusEntry(entry: BolusEntry) { + // Extended bolus entries do not have duration stored, do not use them. + if (entry.extendedDose > 0) return + + if (entry.dateTime > lastBolusDateTime) { + lastBolusDateTime = entry.dateTime + pump.lastBolus = entry + rxBus.send(EventApexPumpDataChanged()) + } + + // Find the bolus in history and sync it. + // Pump may round up boluses, use 0.11 for failsafe. + val ipb = pump.inProgressBolus + if (waitingForCurrentBolusInHistory && ipb != null && entry.dateTime.millis >= ipb.temporaryId) { + if (!ipb.cancelled && abs(entry.standardDose - ipb.currentDose) > 0.11) return + + val syncResult = pumpSync.syncBolusWithTempId( + timestamp = entry.dateTime.millis, + temporaryId = ipb.temporaryId, + amount = entry.standardPerformed * 0.025, + pumpId = entry.dateTime.millis, + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + type = ipb.detailedBolusInfo.bolusType, + ) + aapsLogger.debug(LTag.PUMP, "Final bolus [${entry.standardDose * 0.025}U -> ${entry.standardPerformed * 0.025}U] sync succeeded? $syncResult") + pump.inProgressBolus = null + waitingForCurrentBolusInHistory = false + _bolusCompletable?.complete(ipb) + _bolusCompletable = null + return + } + + // Otherwise, just sync the bolus with the DB + pumpSync.syncBolusWithPumpId( + timestamp = entry.dateTime.millis, + pumpId = entry.dateTime.millis, + amount = entry.standardPerformed * 0.025, + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + type = null, + ) + aapsLogger.debug(LTag.PUMP, "Synced bolus ${entry.standardPerformed * 0.025}U on ${entry.dateTime}") + } + + // !! Unreliable on 6.25 firmware, TODO: think about solution + private fun onTDDEntry(entry: TDDEntry) { + pumpSync.createOrUpdateTotalDailyDose( + timestamp = entry.dateTime.millis, + pumpId = entry.dateTime.millis, + pumpType = PumpType.APEX_TRUCARE_III, + pumpSerial = apexDeviceInfo.serialNumber, + bolusAmount = entry.bolus * 0.025, + basalAmount = entry.basal * 0.025 + entry.temporaryBasal * 0.025, + totalAmount = entry.total * 0.025, + ) + aapsLogger.debug(LTag.PUMP, "Synced TDD ${entry.total * 0.025}U on ${entry.dateTime}") + } + + //////// BLE + + private fun onInitialConnection() { + preferences.put(ApexDoubleKey.MaxBasal, 0.0) + preferences.put(ApexDoubleKey.MaxBolus, 0.0) + pumpSync.connectNewPump() + } + + fun startConnection() { + if (apexDeviceInfo.serialNumber.isNotEmpty()) apexBluetooth.connect() + } + + fun disconnect() { + if (apexBluetooth.status != ApexBluetooth.Status.DISCONNECTED) apexBluetooth.disconnect() + } + + + override fun onConnect() = Thread { + aapsLogger.debug(LTag.PUMPCOMM, "onConnect") + + val version = getValue(GetValue.Value.Version)?.firstOrNull() + if (version !is Version) { + aapsLogger.error(LTag.PUMPCOMM, "Failed to get version - disconnecting.") + return@Thread disconnect() + } + + pump.firmwareVersion = version + + if (!version.isSupported(FIRST_SUPPORTED_PROTO, LAST_SUPPORTED_PROTO)) { + aapsLogger.error(LTag.PUMPCOMM, "Unsupported protocol v${version.protocolMajor}.${version.protocolMinor} - disconnecting.") + uiInteraction.addNotification( + Notification.PUMP_ERROR, + rh.gs(R.string.notification_pump_unsupported), + Notification.URGENT, + ) + return@Thread disconnect() + } + + onVersion(version) + aapsLogger.debug(LTag.PUMPCOMM, "Protocol v${version.protocolMajor}.${version.protocolMinor}") + + if (!syncDateTime("BLE-onConnect")) return@Thread + if (!notifyAboutConnection("BLE-onConnect")) return@Thread + + if (apexDeviceInfo.serialNumber != preferences.get(ApexStringKey.LastConnectedSerialNumber)) { + onInitialConnection() + preferences.put(ApexStringKey.LastConnectedSerialNumber, apexDeviceInfo.serialNumber) + } + + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) + if (!getStatus("BLE-onConnect")) return@Thread + if (!getBoluses("BLE-onConnect")) return@Thread + + unreachableTimerTask?.cancel() + unreachableTimerTask = null + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) + pump.gettingReady = false + }.start() + + private var isDisconnectLoopRunning = false + private fun spawnLoop() { + if (isDisconnectLoopRunning) return + isDisconnectLoopRunning = true + Thread { + while (connectionStatus != ApexBluetooth.Status.CONNECTED) { + if (connectionStatus == ApexBluetooth.Status.DISCONNECTED) { + aapsLogger.debug(LTag.PUMPCOMM, "Starting connection loop") + startConnection() + } + SystemClock.sleep(250) + } + aapsLogger.debug(LTag.PUMPCOMM, "Exiting") + isDisconnectLoopRunning = false + }.start() + } + + override fun onDisconnect() = Thread { + aapsLogger.debug(LTag.PUMPCOMM, "onDisconnect") + getValueResult.clear() + synchronized(getValueResult) { + getValueResult.notifyAll() + } + commandResponse.clear() + synchronized(commandResponse) { + commandResponse.notifyAll() + } + lastConnectedTimestamp = System.currentTimeMillis() + + if (unreachableTimerTask == null) + unreachableTimerTask = timer.schedule(60000) { + uiInteraction.addNotification( + Notification.PUMP_UNREACHABLE, + rh.gs(R.string.error_pump_unreachable), + Notification.URGENT, + ) + aapsLogger.error(LTag.PUMP, "Pump unreachable!") + } + + spawnLoop() + pump.gettingReady = true + }.start() + + private var isGetThreadRunning = false + override fun onPumpCommand(command: PumpCommand) = Thread { + if (command.id == null) { + aapsLogger.error(LTag.PUMPCOMM, "Invalid command with crc ${command.checksum}") + return@Thread + } + val type = PumpObject.findObject(command.id!!, command.objectType, command.objectData) + aapsLogger.debug(LTag.PUMPCOMM, "from PUMP: ${command.id!!.name}, ${type?.name}") + + when (type) { + PumpObject.CommandResponse -> onCommandResponse(CommandResponse(command)) + PumpObject.StatusV1 -> onStatusV1(StatusV1(command)) + PumpObject.Heartbeat -> onHeartbeat() + PumpObject.BolusEntry -> onBolusEntry(BolusEntry(command)) + PumpObject.TDDEntry -> onTDDEntry(TDDEntry(command)) + else -> {} + } + + if (!getValueResult.waiting) return@Thread + if (type != getValueResult.targetObject) return@Thread + + getValueResult.add( + when (type) { + PumpObject.Heartbeat -> Heartbeat() + PumpObject.CommandResponse -> CommandResponse(command) + PumpObject.StatusV1 -> StatusV1(command) + PumpObject.BasalProfile -> BasalProfile(command) + PumpObject.AlarmEntry -> AlarmObject(command) + PumpObject.TDDEntry -> TDDEntry(command) + PumpObject.BolusEntry -> BolusEntry(command) + PumpObject.FirmwareEntry -> Version(command) + else -> return@Thread + } + ) + + if (getValueResult.isSingleObject) { + getValueResult.waiting = false + synchronized(getValueResult) { + getValueResult.notifyAll() + } + } else { + aapsLogger.debug(LTag.PUMPCOMM, "Updating last timestamp") + getValueLastTaskTimestamp = System.currentTimeMillis() + runGetThread() + } + }.start() + + private fun runGetThread() { + if (isGetThreadRunning) return + isGetThreadRunning = true + Thread { + while (true) { + val now = System.currentTimeMillis() + if (now - getValueLastTaskTimestamp >= 500) { + break + } else { + aapsLogger.debug(LTag.PUMPCOMM, "Response is not ready yet") + } + SystemClock.sleep(250) + } + isGetThreadRunning = false + + aapsLogger.debug(LTag.PUMPCOMM, "Chunked response has completed") + getValueResult.waiting = false + synchronized(getValueResult) { + getValueResult.notifyAll() + } + }.start() + } + + //////// Binder + + private val binder = LocalBinder() + override fun onBind(intent: Intent?): IBinder { + aapsLogger.debug(LTag.PUMP, "Binding service") + return binder + } + + inner class LocalBinder : Binder() { + val serviceInstance: ApexService + get() = this@ApexService + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + aapsLogger.debug(LTag.PUMP, "Service started") + return START_STICKY + } +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt new file mode 100644 index 000000000000..5a4815855f72 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt @@ -0,0 +1,309 @@ +package app.aaps.pump.apex.connectivity + +import app.aaps.pump.apex.interfaces.ApexBluetoothCallback +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.ParcelUuid +import android.os.SystemClock +import androidx.core.app.ActivityCompat +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged +import app.aaps.core.keys.Preferences +import app.aaps.core.ui.toast.ToastUtils +import app.aaps.core.utils.toHex +import app.aaps.pump.apex.R +import app.aaps.pump.apex.connectivity.commands.device.DeviceCommand +import app.aaps.pump.apex.connectivity.commands.pump.PumpCommand +import app.aaps.pump.apex.utils.keys.ApexStringKey +import kotlinx.coroutines.sync.Mutex +import java.util.UUID +import javax.inject.Inject + +class ApexBluetooth @Inject constructor( + val aapsLogger: AAPSLogger, + val preferences: Preferences, + val context: Context, + val rxBus: RxBus, +) : ScanCallback() { + companion object { + private val READ_SERVICE = ParcelUuid.fromString("0000FFE0-0000-1000-8000-00805F9B34FB") + private val WRITE_SERVICE = ParcelUuid.fromString("0000FFE5-0000-1000-8000-00805F9B34FB") + + private val READ_UUID = UUID.fromString("0000FFE4-0000-1000-8000-00805F9B34FB") + private val WRITE_UUID = UUID.fromString("0000FFE9-0000-1000-8000-00805F9B34FB") + private val CCC_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB") + + private const val WRITE_DELAY_MS = 250 + } + + private val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter + private var callback: ApexBluetoothCallback? = null + + private var bluetoothDevice: BluetoothDevice? = null + private var bluetoothGatt: BluetoothGatt? = null + private var writeCharacteristic: BluetoothGattCharacteristic? = null + private var readCharacteristic: BluetoothGattCharacteristic? = null + + private var mtu: Int = 512 + + private val readMutex = Mutex() + private var lastCommand: PumpCommand? = null + private var _status: Status = Status.DISCONNECTED + + val status: Status + get() = _status + + fun setCallback(callback: ApexBluetoothCallback) { + this.callback = callback + } + + @Suppress("DEPRECATION") + @SuppressLint("MissingPermission") + @Synchronized + fun send(command: DeviceCommand) { + if (checkBT()) return + if (status != Status.CONNECTED) return + + Thread { + SystemClock.sleep(WRITE_DELAY_MS.toLong()) + val data = command.serialize() + aapsLogger.debug(LTag.PUMPBTCOMM, "DEVICE -> ${data.toHex()}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + bluetoothGatt!!.writeCharacteristic( + writeCharacteristic!!, + data, + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) + } else { + writeCharacteristic!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + writeCharacteristic!!.setValue(data) + bluetoothGatt!!.writeCharacteristic(writeCharacteristic!!) + } + }.start() + } + + @SuppressLint("MissingPermission") + @Synchronized + fun connect() { + aapsLogger.debug(LTag.PUMPBTCOMM, "Connect") + if (preferences.get(ApexStringKey.SerialNumber).isEmpty()) return + if (checkBT()) return + _status = Status.CONNECTING + if (preferences.get(ApexStringKey.BluetoothAddress).isNotEmpty()) return reconnect() + + aapsLogger.debug(LTag.PUMPBTCOMM, "Scan started") + bluetoothAdapter.bluetoothLeScanner.startScan( + listOf( + ScanFilter.Builder() + .setDeviceName("APEX${preferences.get(ApexStringKey.SerialNumber)}") + .build(), + ScanFilter.Builder() + .setServiceUuid(READ_SERVICE) + .build(), + ScanFilter.Builder() + .setServiceUuid(WRITE_SERVICE) + .build(), + ), + ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build(), this + ) + } + + @SuppressLint("MissingPermission") + @Synchronized + fun disconnect() { + aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnect") + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTING)) + if (checkBT()) return + when (status) { + Status.CONNECTED -> { + bluetoothGatt?.disconnect() + } + Status.CONNECTING -> { + stopScan() + SystemClock.sleep(100) + bluetoothGatt?.close() + SystemClock.sleep(100) + bluetoothGatt = null + } + else -> return + } + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) + } + + private fun checkBT(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + ToastUtils.errorToast(context, context.getString(app.aaps.core.ui.R.string.need_connect_permission)) + aapsLogger.error(LTag.PUMPBTCOMM, "No Bluetooth permission!") + return true + } + + if (bluetoothAdapter == null) { + aapsLogger.error(LTag.PUMPBTCOMM, "No Bluetooth adapter!") + return true + } + return false + } + + @Synchronized + @SuppressLint("MissingPermission") + private fun setupGatt() { + bluetoothGatt = bluetoothDevice!!.connectGatt(context, false, object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + when (newState) { + BluetoothGatt.STATE_DISCONNECTED -> { + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) + _status = Status.DISCONNECTED + aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnected") + Thread { callback?.onDisconnect() }.start() + bluetoothGatt?.close() + } + BluetoothGatt.STATE_CONNECTED -> { + bluetoothGatt?.discoverServices() + aapsLogger.debug(LTag.PUMPBTCOMM, "First stage of connection is done") + } + } + } + + override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { + super.onMtuChanged(gatt, mtu, status) + this@ApexBluetooth.mtu = mtu + } + + @Suppress("DEPRECATION") + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) return + + Thread { + gatt.requestMtu(512) + SystemClock.sleep(150) + + writeCharacteristic = gatt.getService(WRITE_SERVICE.uuid).getCharacteristic(WRITE_UUID) + readCharacteristic = gatt.getService(READ_SERVICE.uuid).getCharacteristic(READ_UUID) + gatt.setCharacteristicNotification(readCharacteristic, true) + + val ccc = readCharacteristic!!.getDescriptor(CCC_UUID) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(ccc, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + } else { + ccc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(ccc) + } + }.start() + } + + override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) { + super.onDescriptorWrite(gatt, descriptor, status) + Thread { + aapsLogger.debug(LTag.PUMPBTCOMM, "Connected | Notification status: $status") + SystemClock.sleep(100) + gatt?.setCharacteristicNotification(readCharacteristic, true) + SystemClock.sleep(100) + _status = Status.CONNECTED + callback?.onConnect() + aapsLogger.debug(LTag.PUMPBTCOMM, "Connected successfully") + }.start() + } + + @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") + override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { + super.onCharacteristicRead(gatt, characteristic, status) + Thread { onPumpData(characteristic, characteristic.value) }.start() + } + + @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + super.onCharacteristicChanged(gatt, characteristic) + Thread { onPumpData(characteristic, characteristic.value) }.start() + } + }, BluetoothDevice.TRANSPORT_LE) + + if (bluetoothGatt == null) { + _status = Status.DISCONNECTED + } + } + + private fun onPumpData(characteristic: BluetoothGattCharacteristic, value: ByteArray) { + aapsLogger.debug(LTag.PUMPBTCOMM, "PUMP <- ${value.toHex()}") + when (characteristic.uuid) { + READ_UUID -> synchronized(readMutex) { + // Update command or create new one + if (lastCommand?.isCompleteCommand() == false) + lastCommand!!.update(value) + else if (value.size > PumpCommand.MIN_SIZE) + lastCommand = PumpCommand(value) + else + aapsLogger.error(LTag.PUMPBTCOMM, "Got invalid command of length ${value.size}") + + while (lastCommand != null && lastCommand!!.isCompleteCommand()) { + if (!lastCommand!!.verify()) { + // TODO: find a better way to do the same thing + aapsLogger.error(LTag.PUMPBTCOMM, "[${lastCommand!!.id?.name}] Command checksum is invalid! Expected ${lastCommand!!.checksum.toHex()}") + return + } + + callback?.onPumpCommand(lastCommand!!) + lastCommand = lastCommand!!.trailing + } + } + } + } + + @SuppressLint("MissingPermission") + @Synchronized + private fun reconnect() { + aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to pump...") + bluetoothDevice = bluetoothAdapter!!.getRemoteDevice(preferences.get(ApexStringKey.BluetoothAddress)) + setupGatt() + } + + @SuppressLint("MissingPermission") + @Synchronized + private fun stopScan() { + aapsLogger.debug(LTag.PUMPBTCOMM, "Scan stopped") + bluetoothAdapter?.bluetoothLeScanner?.stopScan(this) + } + + @SuppressLint("MissingPermission") + @Synchronized + override fun onScanResult(callbackType: Int, result: ScanResult?) { + super.onScanResult(callbackType, result) + if (result == null) { + _status = Status.DISCONNECTED + return + } + aapsLogger.debug(LTag.PUMPBTCOMM, "Found device ${result.device.name}") + stopScan() + preferences.put(ApexStringKey.BluetoothAddress, result.device.address) + reconnect() + } + + enum class Status { + DISCONNECTED, + CONNECTING, + CONNECTED; + + fun toLocalString(rh: ResourceHelper): String = when (this) { + DISCONNECTED -> rh.gs(R.string.overview_connection_status_disconnected) + CONNECTING -> rh.gs(R.string.overview_connection_status_connecting) + CONNECTED -> rh.gs(R.string.overview_connection_status_connected) + } + } +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ProtocolVersion.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ProtocolVersion.kt new file mode 100644 index 000000000000..ec3cee1135b3 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ProtocolVersion.kt @@ -0,0 +1,15 @@ +package app.aaps.pump.apex.connectivity + +enum class ProtocolVersion( + val major: Int, + val minor: Int, +) { + /** The first publicly available protocol. + **/ + PROTO_4_10(4, 10), + + /** * Became incompatible: `UpdateSettings`, `Status` + ** * New commands: `GetLatestTemporaryBasals` + **/ + PROTO_4_11(4, 11), +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/CommandId.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/CommandId.kt new file mode 100644 index 000000000000..ff1bd1163cb7 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/CommandId.kt @@ -0,0 +1,7 @@ +package app.aaps.pump.apex.connectivity.commands + +enum class CommandId(val raw: Int) { + SetValue(0xA1), + GetValue(0xA3), + Heartbeat(0xA5), +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/BaseValueCommand.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/BaseValueCommand.kt new file mode 100644 index 000000000000..114302b44ae5 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/BaseValueCommand.kt @@ -0,0 +1,27 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.connectivity.commands.CommandId +import app.aaps.pump.apex.interfaces.ApexDeviceInfo + +abstract class BaseValueCommand(info: ApexDeviceInfo) : DeviceCommand(info) { + /** Value type, default = 0x35 */ + override val type: Int = 0x35 + + /** Value ID */ + abstract val valueId: Int + + /** Does command write value? */ + abstract val isWrite: Boolean + + /** Padding value, default - AA */ + open val paddingValue: Int = 0xAA + + /** Additional data after auth block */ + open val additionalData: ByteArray = byteArrayOf() + + override val id: CommandId + get() = if (isWrite) CommandId.SetValue else CommandId.GetValue + + override val builtData: ByteArray + get() = byteArrayOf(valueId.toByte(), paddingValue.toByte()) + authBlock + additionalData +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/Bolus.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/Bolus.kt new file mode 100644 index 000000000000..4cf2aa09231f --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/Bolus.kt @@ -0,0 +1,21 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.asShortAsByteArray + +/** Set bolus. + * + * * [dose] - Bolus dose in 0.025U steps + */ +class Bolus( + info: ApexDeviceInfo, + val dose: Int, +) : BaseValueCommand(info) { + override val valueId = 0x12 + override val isWrite = true + + override val additionalData: ByteArray + get() = dose.asShortAsByteArray() + 0x00.toByte() // TODO: find out what does zero mean + + override fun toString(): String = "Bolus($dose)" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelBolus.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelBolus.kt new file mode 100644 index 000000000000..ab170d579d6a --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelBolus.kt @@ -0,0 +1,17 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo + +/** Cancel bolus. */ +class CancelBolus( + info: ApexDeviceInfo, +) : BaseValueCommand(info) { + override val type = 0x55 + override val valueId = 0x02 + override val isWrite = true + + override val additionalData: ByteArray + get() = byteArrayOf(0, 0) // TODO: find out what does it mean + + override fun toString(): String = "CancelBolus()" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelTemporaryBasal.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelTemporaryBasal.kt new file mode 100644 index 000000000000..b57a63c579e3 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/CancelTemporaryBasal.kt @@ -0,0 +1,11 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo + +/** Cancel temporary basal if set before */ +class CancelTemporaryBasal(info: ApexDeviceInfo): BaseValueCommand(info) { + override val valueId = 0x05 + override val isWrite = true + + override fun toString(): String = "CancelTemporaryBasal()" +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DeviceCommand.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DeviceCommand.kt new file mode 100644 index 000000000000..eef7dc1c7bd2 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DeviceCommand.kt @@ -0,0 +1,40 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.connectivity.commands.CommandId +import app.aaps.pump.apex.connectivity.ProtocolVersion +import app.aaps.pump.apex.utils.ApexCrypto +import app.aaps.pump.apex.interfaces.ApexDeviceInfo + +abstract class DeviceCommand(val info: ApexDeviceInfo) { + /** Command type, 0x35 or 0x55. See children classes for more info. */ + open val type = 0x35 + + /** Minimum protocol version supporting this command */ + open val minProto = ProtocolVersion.PROTO_4_10 + + /** Maximum protocol version supporting this command */ + open val maxProto = ProtocolVersion.PROTO_4_11 + + /** Command ID */ + abstract val id: CommandId + + /** Constructed data by children */ + abstract val builtData: ByteArray + + /** Serialize command, ready to be sent via BLE */ + fun serialize(): ByteArray { + val cmdData = builtData + val header = byteArrayOf( + type.toByte(), + (4 + cmdData.size + 2).toByte(), + 0x00, + id.raw.toByte() + ) + val data = header + cmdData + return data + ApexCrypto.crc16(data) + } + + /** Get common auth block between multiple commands */ + protected val authBlock: ByteArray + get() = ("APEX" + info.serialNumber).toByteArray() +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DualBolus.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DualBolus.kt new file mode 100644 index 000000000000..6895fc4c4035 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/DualBolus.kt @@ -0,0 +1,25 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.asShortAsByteArray + +/** Set dual wave bolus. + * + * * [firstDose] - First bolus dose in 0.025U steps + * * [secondDose] - First bolus dose in 0.025U steps + * * [interval] - Interval in 15 minute steps + */ +class DualBolus( + info: ApexDeviceInfo, + val firstDose: Int, + val secondDose: Int, + val interval: Int, +) : BaseValueCommand(info) { + override val valueId = 0x14 + override val isWrite = true + + override val additionalData: ByteArray + get() = firstDose.asShortAsByteArray() + secondDose.asShortAsByteArray() + interval.asShortAsByteArray() + + override fun toString(): String = "DualBolus(first = $firstDose, second = $secondDose, interval = $interval)" +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/ExtendedBolus.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/ExtendedBolus.kt new file mode 100644 index 000000000000..010be90f521d --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/ExtendedBolus.kt @@ -0,0 +1,23 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.asShortAsByteArray + +/** Set extended bolus. + * + * * [dose] - Bolus dose in 0.025U steps + * * [duration] - Duration in 15 minute steps + */ +class ExtendedBolus( + info: ApexDeviceInfo, + val dose: Int, + val duration: Int, +) : BaseValueCommand(info) { + override val valueId = 0x13 + override val isWrite = true + + override val additionalData: ByteArray + get() = dose.asShortAsByteArray() + duration.asShortAsByteArray() + + override fun toString(): String = "ExtendedBolus(dose = $dose, duration = $duration)" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/GetValue.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/GetValue.kt new file mode 100644 index 000000000000..ebb85bc33b4e --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/GetValue.kt @@ -0,0 +1,56 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo + +/** Get pump value. + * + * [value] - Value to get + */ +class GetValue( + info: ApexDeviceInfo, + val value: Value, +) : BaseValueCommand(info) { + override val type = value.type + override val valueId = value.valueId + override val isWrite = false + + override val paddingValue: Int + get() = when (value) { + Value.LatestBoluses -> 0x01 + else -> 0xAA + } + + enum class Value(val valueId: Int, val type: Int = 0x35) { + /** Pump status and settings, proto <=4.10 */ + StatusV1(0x00), + + /** Bolus history for month? It had returned A LOT of bolus entries */ + BolusHistory(0x01, 0x55), + + /** Pump alarms, returns latest 20 ones */ + Alarms(0x03, 0x55), + + /** Latest TDDs */ + TDDs(0x06, 0x55), + + /** Pump bolus wizard status */ + WizardStatus(0x07), + + /** Pump basal profiles */ + BasalProfiles(0x08), + + /** Pump status and settings, proto >=4.11 */ + StatusV2(0x0c), + + /** Latest boluses */ + LatestBoluses(0x21), + + /** Latest temporary basals, proto >=4.11 */ + LatestTemporaryBasals(0x27), + + /** Firmware version */ + Version(0x31), + } + + override fun toString(): String = "GetValue(${value.name})" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/NotifyAboutConnection.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/NotifyAboutConnection.kt new file mode 100644 index 000000000000..ae3efaf05bfb --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/NotifyAboutConnection.kt @@ -0,0 +1,16 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo + +/** Notify pump about connection, should be sent right after connection established. */ +class NotifyAboutConnection( + info: ApexDeviceInfo, +) : BaseValueCommand(info) { + override val valueId = 0x33 + override val isWrite = true + + override val additionalData: ByteArray + get() = byteArrayOf(0x01, 0x00) // TODO: find out what do these values mean + + override fun toString(): String = "NotifyAboutConnection()" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/SyncDateTime.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/SyncDateTime.kt new file mode 100644 index 000000000000..b334de77950d --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/SyncDateTime.kt @@ -0,0 +1,29 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import org.joda.time.DateTime + +/** Update system date and time. + * Sent after every connection and every N pump heartbeats. + * + * * [dateTime] - new date and time. + */ +class SyncDateTime( + info: ApexDeviceInfo, + val dateTime: DateTime, +) : BaseValueCommand(info) { + override val valueId = 0x31 + override val isWrite = true + + override val additionalData: ByteArray + get() = byteArrayOf( + (dateTime.year % 100).toByte(), + dateTime.monthOfYear.toByte(), + dateTime.dayOfMonth.toByte(), + dateTime.hourOfDay.toByte(), + dateTime.minuteOfHour.toByte(), + dateTime.secondOfMinute.toByte() + ) + + override fun toString(): String = "SyncDateTime($dateTime)" +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/TemporaryBasal.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/TemporaryBasal.kt new file mode 100644 index 000000000000..2bfcada0aea1 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/TemporaryBasal.kt @@ -0,0 +1,26 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.asShortAsByteArray +import app.aaps.pump.apex.utils.toByte + +/** Set temporary basal, either absolute or relative. + * + * * [isAbsolute] - Is absolute or relative temporary basal? + * * [duration] - Duration in 15 minute steps + * * [value] - Dose in 0.025U steps if absolute, percentage if relative + */ +class TemporaryBasal( + info: ApexDeviceInfo, + val isAbsolute: Boolean = true, + val duration: Int, + val value: Int, +) : BaseValueCommand(info) { + override val valueId = 0x02 + override val isWrite = true + + override val additionalData: ByteArray + get() = byteArrayOf(isAbsolute.toByte(), duration.toByte()) + value.asShortAsByteArray() + + override fun toString(): String = "TemporaryBasal(absolute = $isAbsolute, duration = $duration, value = $value)" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateBasalProfileRates.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateBasalProfileRates.kt new file mode 100644 index 000000000000..f3dff9edec84 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateBasalProfileRates.kt @@ -0,0 +1,26 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.shortLSB +import app.aaps.pump.apex.utils.shortMSB + +/** Set currently used basal profile rates. + * + * * [rates] - 48 basal rates, one per 30 minute, in 0.025U steps + */ +class UpdateBasalProfileRates( + info: ApexDeviceInfo, + val rates: List +) : BaseValueCommand(info) { + override val valueId = 0x00 + override val isWrite = true + + override val additionalData: ByteArray + get() = ByteArray(96) { + val rate = rates[it / 2] + if (it % 2 == 0) rate.shortLSB() + else rate.shortMSB() + } + + override fun toString(): String = "UpdateBasalProfileRates(${rates.joinToString(", ", "[", "]")})" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt new file mode 100644 index 000000000000..de721f3c63a9 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt @@ -0,0 +1,89 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.connectivity.ProtocolVersion +import app.aaps.pump.apex.connectivity.commands.pump.AlarmLength +import app.aaps.pump.apex.connectivity.commands.pump.AlarmType +import app.aaps.pump.apex.connectivity.commands.pump.BolusDeliverySpeed +import app.aaps.pump.apex.connectivity.commands.pump.Language +import app.aaps.pump.apex.connectivity.commands.pump.ScreenBrightness +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.asShortAsByteArray + +/** Update system settings. + * + * * [lockKeys] - Lock pump keyboard after some time + * * [limitTDD] - Limit total daily dose amount + * * [language] - System language + * * [bolusSpeed] - Bolus speed + * * [alarmType] - Pump alarms type + * * [alarmLength] - Pump alarms length + * * [screenBrightness] - Screen brightness + * * [lowReservoirThreshold] - Threshold in U for low reservoir warning + * * [lowReservoirDurationThreshold] - Threshold in steps of 30 minutes for low reservoir warning (appears when reservoir will be empty in less than N minutes) + * * [enableAdvancedBolus] - Enable Dual and Extended bolus + * * [screenDisableDuration] - Screen disable duration in 0.1 second steps + * * [maxTDD] - TDD alarm threshold + * * [maxBasalRate] - Maximum basal rate and TBR value in 0.025U steps + * * [maxSingleBolus] - Maximum single bolus in 0.025U steps + */ +class UpdateSettingsV1( + info: ApexDeviceInfo, + val lockKeys: Boolean, + val limitTDD: Boolean, + val language: Language, + val bolusSpeed: BolusDeliverySpeed, + val alarmType: AlarmType, + val alarmLength: AlarmLength, + val screenBrightness: ScreenBrightness, + val lowReservoirThreshold: Int, + val lowReservoirDurationThreshold: Int, + val enableAdvancedBolus: Boolean, + val screenDisableDuration: Int, + val maxTDD: Int, + val maxBasalRate: Int, + val maxSingleBolus: Int, +) : BaseValueCommand(info) { + override val valueId = 0x32 + override val isWrite = true + + override val maxProto = ProtocolVersion.PROTO_4_10 + + override val additionalData: ByteArray + get() { + var functionFlags = 0 + var bolusFlags = 1 shl 1 + + if (bolusSpeed == BolusDeliverySpeed.Low) functionFlags = functionFlags or FunctionFlags.LowBolusSpeed.raw + if (language == Language.English) functionFlags = functionFlags or FunctionFlags.EnglishLanguage.raw + if (limitTDD) functionFlags = functionFlags or FunctionFlags.TDDLimit.raw + if (lockKeys) functionFlags = functionFlags or FunctionFlags.KeyboardLock.raw + if (enableAdvancedBolus) bolusFlags = bolusFlags or BolusFlags.AdvancedBolus.raw + + return byteArrayOf( + functionFlags.toByte(), + alarmType.raw, + screenBrightness.raw, + 1, // unknown + lowReservoirThreshold.toByte(), + lowReservoirDurationThreshold.toByte(), + bolusFlags.toByte(), + alarmLength.raw + ) + screenDisableDuration.asShortAsByteArray() + + maxTDD.asShortAsByteArray() + + maxBasalRate.asShortAsByteArray() + + maxSingleBolus.asShortAsByteArray() + } + + private enum class FunctionFlags(val raw: Int) { + LowBolusSpeed(1 shl 0), + KeyboardLock(1 shl 1), + TDDLimit(1 shl 2 and 1 shl 4), + EnglishLanguage(1 shl 3 and 1 shl 5), + } + + private enum class BolusFlags(val raw: Int) { + AdvancedBolus(1 shl 0), + } + + override fun toString(): String = "UpdateSettingsV1(maxTDD = $maxTDD, maxBolus = $maxSingleBolus, maxBasal = $maxBasalRate, bolusSpeed = ${bolusSpeed.name}, ...)" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSystemState.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSystemState.kt new file mode 100644 index 000000000000..7d9e467cb3ea --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSystemState.kt @@ -0,0 +1,21 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.toByte + +/** Update system state, suspend or resume. + * + * * [isSuspended] - Suspend system? + */ +class UpdateSystemState( + info: ApexDeviceInfo, + val isSuspended: Boolean = true, +) : BaseValueCommand(info) { + override val valueId = 0x21 + override val isWrite = true + + override val additionalData: ByteArray + get() = byteArrayOf(isSuspended.toByte()) + + override fun toString(): String = "UpdateSystemState(suspended = $isSuspended)" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateUsedBasalProfile.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateUsedBasalProfile.kt new file mode 100644 index 000000000000..637b48657afa --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateUsedBasalProfile.kt @@ -0,0 +1,20 @@ +package app.aaps.pump.apex.connectivity.commands.device + +import app.aaps.pump.apex.interfaces.ApexDeviceInfo + +/** Set basal profile [index] to be used now. + * + * * [index] - Basal profile index + */ +class UpdateUsedBasalProfile( + info: ApexDeviceInfo, + val index: Int, +) : BaseValueCommand(info) { + override val valueId = 0x04 + override val isWrite = true + + override val additionalData: ByteArray + get() = byteArrayOf(index.toByte()) + + override fun toString(): String = "UpdateUsedBasalProfile(id = $index)" +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/AlarmObject.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/AlarmObject.kt new file mode 100644 index 000000000000..33629bddfa2c --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/AlarmObject.kt @@ -0,0 +1,23 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.pump.apex.utils.getUnsignedShort +import app.aaps.pump.apex.utils.hexAsDecToDec +import org.joda.time.DateTime + +class AlarmObject(command: PumpCommand): PumpObjectModel() { + /** Alarm entry index */ + val index = command.objectData[1].toUByte().toInt() + + /** Alarm date */ + val dateTime = DateTime( + command.objectData[2].hexAsDecToDec() + 2000, // year + command.objectData[3].hexAsDecToDec(), // day + command.objectData[4].hexAsDecToDec(), // month + command.objectData[5].hexAsDecToDec(), // hour + command.objectData[6].hexAsDecToDec(), // minute + command.objectData[7].hexAsDecToDec(), // second + ) + + /** Alarm type */ + val type = Alarm.entries.find { it.raw == (getUnsignedShort(command.objectData, 8) + 0x100) } +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BasalProfile.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BasalProfile.kt new file mode 100644 index 000000000000..9d8f12d2a2e5 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BasalProfile.kt @@ -0,0 +1,13 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.pump.apex.utils.getUnsignedShort + +class BasalProfile(command: PumpCommand): PumpObjectModel() { + /** Basal profile index */ + val index = command.objectData[1].toUByte().toInt() + + /** Basal profile rates, in 0.025U steps, for every 30 minutes */ + val rates: List = List(48) { + getUnsignedShort(command.objectData, 2 + it * 2) + } +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BolusEntry.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BolusEntry.kt new file mode 100644 index 000000000000..11e293a1e958 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/BolusEntry.kt @@ -0,0 +1,43 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.pump.apex.R +import app.aaps.pump.apex.utils.getUnsignedShort +import app.aaps.pump.apex.utils.hexAsDecToDec +import org.joda.time.DateTime + +class BolusEntry(command: PumpCommand): PumpObjectModel() { + /** Bolus entry index */ + val index = command.objectData[1].toUByte().toInt() + + /** Bolus date */ + val dateTime = DateTime( + command.objectData[2].hexAsDecToDec() + 2000, // year + command.objectData[3].hexAsDecToDec(), // day + command.objectData[4].hexAsDecToDec(), // month + command.objectData[5].hexAsDecToDec(), // hour + command.objectData[6].hexAsDecToDec(), // minute + command.objectData[7].hexAsDecToDec(), // second + ) + + /** Standard bolus requested dose */ + val standardDose = getUnsignedShort(command.objectData, 8) + + /** Standard bolus actual dose */ + val standardPerformed = getUnsignedShort(command.objectData, 10) + + /** Extended bolus requested dose */ + val extendedDose = getUnsignedShort(command.objectData, 12) + + /** Extended bolus actual dose */ + val extendedPerformed = getUnsignedShort(command.objectData, 14) + + fun toShortLocalString(rh: ResourceHelper): String { + val diff = System.currentTimeMillis() - dateTime.millis + if (diff >= 60 * 60 * 1000) { + return rh.gs(R.string.overview_pump_last_bolus_h, standardPerformed * 0.025, diff / 60 / 60 / 1000, (diff / 60 / 1000) % 60) + } else { + return rh.gs(R.string.overview_pump_last_bolus_min, standardPerformed * 0.025, diff / 60 / 1000) + } + } +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/CommandResponse.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/CommandResponse.kt new file mode 100644 index 000000000000..57d9ecf7cfc4 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/CommandResponse.kt @@ -0,0 +1,20 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.pump.apex.utils.getUnsignedShort + +class CommandResponse(command: PumpCommand): PumpObjectModel() { + /** Command response code */ + val code = Code.entries.find { it.raw.toByte() == command.objectData[0] } ?: Code.Unknown + + /** Bolus dose if present */ + val dose = getUnsignedShort(command.objectData, 2) + + enum class Code(val raw: Int) { + Accepted(0x55), + Invalid(0xA5), + Completed(0xAA), + StandardBolusProgress(0xA0), + ExtendedBolusProgress(0xA1), + Unknown(0xBADC0DE) + } +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Heartbeat.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Heartbeat.kt new file mode 100644 index 000000000000..6f362c5621d2 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Heartbeat.kt @@ -0,0 +1,3 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +class Heartbeat: PumpObjectModel() diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt new file mode 100644 index 000000000000..a64f587da944 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt @@ -0,0 +1,63 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.pump.apex.connectivity.commands.CommandId +import androidx.annotation.VisibleForTesting +import app.aaps.pump.apex.utils.ApexCrypto + +// Read-only commands which we get from the pump. +// Format is: +// AA [UByte length*] [UByte flags] [UByte id] [data...] [UShort crc] +// * - heartbeat (a5) has incorrect length, 06 expected vs 08 actual. +class PumpCommand(private var data: ByteArray) { + companion object { + const val MIN_SIZE = 6 + } + + // Use getters here cause data may be changed + val type: Int get() = data[0].toUByte().toInt() // Should always be AA + val length: Int get() = data[1].toUByte().toInt() // May be greater or less than packet length! + + val objectType: Int get() = data[2].toUByte().toInt() + val id: CommandId? get() = CommandId.entries.find { it.raw.toUByte() == data[3].toUByte() } + val objectData: ByteArray get() = data.copyOfRange(4, realLength() - 2) + + val checksum: ByteArray + get() = data.copyOfRange(data.size - 2, data.size) + + private fun realLength(): Int = + if (id == CommandId.Heartbeat) + length + 2 + else length + + @VisibleForTesting + fun calculatedChecksum(): ByteArray { + return ApexCrypto.crc16(data, realLength() - 2) + } + + /** Verify checksum */ + fun verify(): Boolean { + val calc = calculatedChecksum() + return calc.contentEquals(checksum) + } + + /** Is command complete? */ + fun isCompleteCommand(): Boolean { + return data.size >= realLength() + } + + /** Returns the next trailing command if present. */ + val trailing: PumpCommand? + get() { + // Trailing is present only on GetValue + if (id != CommandId.GetValue) return null + if (data.size <= length) return null + if (data.size - length < MIN_SIZE) return null + return PumpCommand(data.copyOfRange(length, data.size)) + } + + /** Add remaining data to the command. */ + fun update(remainingData: ByteArray): Boolean { + data += remainingData + return isCompleteCommand() + } +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt new file mode 100644 index 000000000000..f706c30df9a7 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt @@ -0,0 +1,105 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.pump.apex.connectivity.commands.CommandId + +enum class PumpObject( + val commandId: CommandId = CommandId.GetValue, + val objectId: Int? = null, + val valueId: Int? = null, +) { + Heartbeat(commandId = CommandId.Heartbeat), + CommandResponse(commandId = CommandId.SetValue), + StatusV1(objectId = 0x01, valueId = 0x00), + WizardStatus(objectId = 0x01, valueId = 0x07), + BasalProfile(objectId = 0x08), + AlarmEntry(objectId = 0x14), + TDDEntry(objectId = 0x5c), + BolusEntry(objectId = 0x80), + FirmwareEntry(objectId = 0x00, valueId = 0x31); + + companion object { + fun findObject(commandId: CommandId, objectId: Int, objectData: ByteArray): PumpObject? { + for (e in entries) { + if (commandId != e.commandId) continue + if (e.objectId == null) return e + if (objectId != e.objectId) continue + if (e.valueId == null) return e + if (objectData[0].toInt() != e.valueId) continue + return e + } + return null + } + } +} + +abstract class PumpObjectModel + +enum class BatteryLevel(val raw: Byte, val approximatePercentage: Int) { + Dead(0, 0), + Low(1, 25), + Medium(2, 50), + High(3, 75), + Full(4, 100), +} + +enum class Language(val raw: Byte) { + Russian(0), + English(1), +} + +enum class ScreenBrightness(val raw: Byte) { + P10(0), + P30(1), + P50(2), + P60(3), + P80(4), + P100(5), +} + +enum class AlarmType(val raw: Byte) { + Sound(0), + Vibration(1), + VibrationAndSound(2), +} + +enum class AlarmLength(val raw: Byte) { + Long(0), + Medium(1), + Short(2), +} + +enum class BolusDeliverySpeed(val raw: Byte) { + Standard(0), + Low(1), +} + +enum class Alarm(val raw: Int) { + Unknown(0xBADC0DE), + NoError(0x100), + LowBattery(0x101), + CheckGlucose(0x102), + ButtonError(0x103), + LowReservoir(0x104), + DeadBattery(0x105), + BatteryError(0x106), + TimeError(0x107), + NoDelivery(0x108), + ResetError(0x109), + CommunicationError(0x10a), + MotorError(0x10b), + EncoderError(0x10c), + NoDosage(0x10d), + TDDLimitTriggered(0x10e), + NoReservoir(0x10f), + ScreenError(0x201), + FRAMError(0x202), + TimeAnomalyError(0x203), + ClockError(0x204), + Reserved1(0x205), + MotorAbnormal(0x206), + MotorPowerAbnormal(0x207), + BolusOrBasalDoseAbnormal(0x208), + ConnectionAnomaly(0x209), + Reserved2(0x20a), + PressureAbnormal(0x20b), +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt new file mode 100644 index 000000000000..039603d7e257 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt @@ -0,0 +1,154 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.pump.apex.connectivity.commands.device.UpdateSettingsV1 +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.getUnsignedInt +import app.aaps.pump.apex.utils.getUnsignedShort +import app.aaps.pump.apex.utils.hexAsDecToDec +import app.aaps.pump.apex.utils.toBoolean +import org.joda.time.DateTime + +class StatusV1(command: PumpCommand): PumpObjectModel() { + /** Pump approximate battery level */ + val batteryLevel = BatteryLevel.entries.find { it.raw == command.objectData[2] } + + /** Alarm type */ + val alarmType = AlarmType.entries.find { it.raw == command.objectData[3] } + + /** Bolus delivery speed */ + val deliverySpeed = BolusDeliverySpeed.entries.find { it.raw == command.objectData[4] } + + /** Screen brightness */ + val brightness = ScreenBrightness.entries.find { it.raw == command.objectData[5] } + + private val bolusFlags = command.objectData[6].toUByte().toInt() + private enum class BolusFlags(val raw: Int) { + AdvancedBolusEnabled(1 shl 1) + } + + /** Are dual and extended bolus types enabled? */ + val advancedBolusEnabled = (bolusFlags and BolusFlags.AdvancedBolusEnabled.raw) == 1 + + /** Keys lock enabled? */ + val keyboardLockEnabled = command.objectData[7].toBoolean() + + /** Pump auto-suspend enabled? */ + val autoSuspendEnabled = command.objectData[8].toBoolean() + + /** Time for auto-suspend to trigger, in 30 minute steps */ + val autoSuspendDuration = command.objectData[9].toUByte().toInt() + + /** Low reservoir alarm threshold in 1U steps */ + val lowReservoirThreshold = command.objectData[10].toUByte().toInt() + + /** Low reservoir alarm (triggered by time left) threshold in 30 minute steps */ + val lowReservoirTimeLeftThreshold = command.objectData[11].toUByte().toInt() + + // Byte 10, 11 unknown + + /** Current basal pattern index */ + val currentBasalPattern = command.objectData[14].toUByte().toInt() + + /** Is TDD limit enabled? */ + val totalDailyDoseLimitEnabled = command.objectData[15].toBoolean() + + /** Screen disable timeout, in 0.1s steps */ + val screenTimeout = getUnsignedShort(command.objectData, 16) + + /** Current TDD */ + val totalDailyDose = getUnsignedInt(command.objectData, 18) + + /** TDD alarm threshold */ + val maxTDD = getUnsignedInt(command.objectData, 22) + + /** Maximum basal rate in 0.025U steps */ + val maxBasal = getUnsignedShort(command.objectData, 26) + + /** Maximum bolus in 0.025U steps */ + val maxBolus = getUnsignedShort(command.objectData, 28) + + // Byte 29-45 unknown + + /** System date and time */ + val dateTime = DateTime( + command.objectData[46].toUByte().toInt() + 2000, // year + command.objectData[47].toUByte().toInt(), // month + command.objectData[48].toUByte().toInt(), // day + command.objectData[49].toUByte().toInt(), // hour + command.objectData[50].toUByte().toInt(), // minute + command.objectData[51].toUByte().toInt(), // second + ) + + /** System language */ + val language = Language.entries.find { it.raw == command.objectData[52] } + + /** Is temporary basal active? */ + val isTemporaryBasalActive = command.objectData[53].toBoolean() + + /** Reservoir level, last 3 numbers are decimals */ + val reservoir = getUnsignedInt(command.objectData, 54) + + /** Current alarms list */ + val alarms = buildList { + for (i in 0..<9) { + val raw = getUnsignedShort(command.objectData, 58 + 2 * i) + if (raw != 0) add(Alarm.entries.find { it.raw == raw } ?: Alarm.Unknown) + } + } + + /** Current basal rate in 0.025U steps */ + val currentBasalRate = getUnsignedShort(command.objectData, 78) + + /** Current basal rate end time */ + val currentBasalEndHour = command.objectData[80].toUByte().toInt() + /** Current basal rate end time */ + val currentBasalEndMinute = command.objectData[81].toUByte().toInt() + + /** TBR if present */ + val temporaryBasalRate = getUnsignedShort(command.objectData, 82) + + /** Is TBR absolute? */ + val temporaryBasalRateIsAbsolute = command.objectData[84].toBoolean() + + /** TBR duration, in 1 minute steps */ + val temporaryBasalRateDuration = getUnsignedShort(command.objectData, 86) + + /** TBR elapsed time, in 1 minute steps */ + val temporaryBasalRateElapsed = getUnsignedShort(command.objectData, 88) + + fun toUpdateSettingsV1( + info: ApexDeviceInfo, + alarmLength: AlarmLength, + lockKeys: Boolean? = null, + limitTDD: Boolean? = null, + language: Language? = null, + bolusSpeed: BolusDeliverySpeed? = null, + alarmType: AlarmType? = null, + screenBrightness: ScreenBrightness? = null, + lowReservoirThreshold: Int? = null, + lowReservoirDurationThreshold: Int? = null, + enableAdvancedBolus: Boolean? = null, + screenDisableDuration: Int? = null, + maxTDD: Int? = null, + maxBasalRate: Int? = null, + maxSingleBolus: Int? = null, + ): UpdateSettingsV1 { + return UpdateSettingsV1( + info, + lockKeys ?: keyboardLockEnabled, + limitTDD ?: totalDailyDoseLimitEnabled, + language ?: this.language!!, + bolusSpeed ?: deliverySpeed!!, + alarmType ?: this.alarmType!!, + alarmLength, + screenBrightness ?: this.brightness!!, + lowReservoirThreshold ?: this.lowReservoirThreshold, + lowReservoirDurationThreshold ?: this.lowReservoirTimeLeftThreshold, + enableAdvancedBolus ?: this.advancedBolusEnabled, + screenDisableDuration ?: this.screenTimeout, + maxTDD ?: this.maxTDD, + maxBasalRate ?: this.maxBasal, + maxSingleBolus ?: this.maxBolus, + ) + } +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/TDDEntry.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/TDDEntry.kt new file mode 100644 index 000000000000..c033126f3975 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/TDDEntry.kt @@ -0,0 +1,30 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.pump.apex.utils.getUnsignedShort +import app.aaps.pump.apex.utils.hexAsDecToDec +import org.joda.time.DateTime + +class TDDEntry(command: PumpCommand): PumpObjectModel() { + /** TDD entry index */ + val index = command.objectData[1].toUByte().toInt() + + /** Bolus part of TDD */ + val bolus = getUnsignedShort(command.objectData, 2) + + /** Basal part of TDD */ + val basal = getUnsignedShort(command.objectData, 4) + + /** Temporary basal part of TDD */ + val temporaryBasal = getUnsignedShort(command.objectData, 6) + + /** TDD */ + val total = bolus + basal + temporaryBasal + + /** TDD entry date */ + val dateTime = DateTime( + command.objectData[8].hexAsDecToDec() + 2000, + command.objectData[9].hexAsDecToDec(), + command.objectData[10].hexAsDecToDec(), + 0, 0 + ) +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt new file mode 100644 index 000000000000..0dd016f1dfcf --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt @@ -0,0 +1,34 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.pump.apex.R +import app.aaps.pump.apex.connectivity.ProtocolVersion + +class Version(command: PumpCommand): PumpObjectModel() { + /** Firmware major part of version */ + val firmwareMajor = command.objectData[6].toUByte().toInt() + + /** Firmware minor part of version */ + val firmwareMinor = command.objectData[7].toUByte().toInt() + + /** Protocol major part of version */ + val protocolMajor = command.objectData[8].toUByte().toInt() + + /** Protocol minor part of version */ + val protocolMinor = command.objectData[9].toUByte().toInt() + + fun toLocalString(rh: ResourceHelper): String { + return rh.gs(R.string.overview_pump_fw, firmwareMajor, firmwareMinor, protocolMajor, protocolMinor) + } + + fun atleastProto(proto: ProtocolVersion): Boolean { + return protocolMajor >= proto.major && protocolMinor >= proto.minor + } + + fun isSupported(min: ProtocolVersion, max: ProtocolVersion): Boolean { + if (min.major > protocolMajor || max.major < protocolMajor) return false + if (max.major > protocolMajor) return true + if (max.minor < protocolMinor) return false + return true + } +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexModule.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexModule.kt new file mode 100644 index 000000000000..964af320f9f1 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexModule.kt @@ -0,0 +1,9 @@ +package app.aaps.pump.apex.di + +import dagger.Module + +@Module(includes = [ + ApexUiModule::class, + ApexServicesModule::class, +]) +open class ApexModule diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexServicesModule.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexServicesModule.kt new file mode 100644 index 000000000000..9289616cea9c --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexServicesModule.kt @@ -0,0 +1,18 @@ +package app.aaps.pump.apex.di + +import app.aaps.pump.apex.ApexService +import app.aaps.pump.apex.connectivity.ApexBluetooth +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.misc.ApexDeviceInfoImpl +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +@Suppress("unused") +abstract class ApexServicesModule { + @Binds abstract fun contributesApexDeviceInfo(apexDeviceInfoImpl: ApexDeviceInfoImpl): ApexDeviceInfo + @ContributesAndroidInjector abstract fun contributesApexBluetooth(): ApexBluetooth + @ContributesAndroidInjector abstract fun contributesApexService(): ApexService +} + diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexUiModule.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexUiModule.kt new file mode 100644 index 000000000000..bcb4ac92cd27 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/di/ApexUiModule.kt @@ -0,0 +1,11 @@ +package app.aaps.pump.apex.di + +import app.aaps.pump.apex.ui.ApexFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +@Suppress("unused") +abstract class ApexUiModule { + @ContributesAndroidInjector abstract fun contributesApexFragment(): ApexFragment +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/events/EventApexPumpDataChanged.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/events/EventApexPumpDataChanged.kt new file mode 100644 index 000000000000..632946439a58 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/events/EventApexPumpDataChanged.kt @@ -0,0 +1,5 @@ +package app.aaps.pump.apex.events + +import app.aaps.core.interfaces.rx.events.Event + +class EventApexPumpDataChanged : Event() diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexBluetoothCallback.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexBluetoothCallback.kt new file mode 100644 index 000000000000..edb5477ebf52 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexBluetoothCallback.kt @@ -0,0 +1,9 @@ +package app.aaps.pump.apex.interfaces + +import app.aaps.pump.apex.connectivity.commands.pump.PumpCommand + +interface ApexBluetoothCallback { + fun onConnect() + fun onDisconnect() + fun onPumpCommand(command: PumpCommand) +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexDeviceInfo.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexDeviceInfo.kt new file mode 100644 index 000000000000..ea2d36363884 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/interfaces/ApexDeviceInfo.kt @@ -0,0 +1,5 @@ +package app.aaps.pump.apex.interfaces + +interface ApexDeviceInfo { + var serialNumber: String +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/misc/ApexDeviceInfoImpl.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/misc/ApexDeviceInfoImpl.kt new file mode 100644 index 000000000000..a2e5118566f3 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/misc/ApexDeviceInfoImpl.kt @@ -0,0 +1,16 @@ +package app.aaps.pump.apex.misc + +import app.aaps.core.keys.Preferences +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.pump.apex.utils.keys.ApexStringKey +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ApexDeviceInfoImpl @Inject constructor( + val preferences: Preferences +): ApexDeviceInfo { + override var serialNumber: String + get() = preferences.get(ApexStringKey.SerialNumber) + set(s) = preferences.put(ApexStringKey.SerialNumber, s) +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ui/ApexFragment.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ui/ApexFragment.kt new file mode 100644 index 000000000000..093302659e84 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ui/ApexFragment.kt @@ -0,0 +1,114 @@ +package app.aaps.pump.apex.ui + +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import app.aaps.core.data.time.T +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.AapsSchedulers +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.EventInitializationChanged +import app.aaps.core.interfaces.rx.events.EventPreferenceChange +import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged +import app.aaps.core.interfaces.rx.events.EventQueueChanged +import app.aaps.core.interfaces.rx.events.EventTempBasalChange +import app.aaps.core.interfaces.utils.fabric.FabricPrivacy +import app.aaps.pump.apex.ApexPump +import app.aaps.pump.apex.R +import app.aaps.pump.apex.databinding.ApexFragmentBinding +import app.aaps.pump.apex.events.EventApexPumpDataChanged +import dagger.android.support.DaggerFragment +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import javax.inject.Inject + +class ApexFragment : DaggerFragment() { + @Inject lateinit var activePlugin: ActivePlugin + @Inject lateinit var pump: ApexPump + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var aapsSchedulers: AapsSchedulers + @Inject lateinit var rxBus: RxBus + @Inject lateinit var fabricPrivacy: FabricPrivacy + + private val disposable = CompositeDisposable() + private val handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) + private var refreshLoop: Runnable = Runnable { + activity?.runOnUiThread { updateGUI() } + } + + private var _binding: ApexFragmentBinding? = null + val binding: ApexFragmentBinding + get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + _binding = ApexFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } + + override fun onResume() { + super.onResume() + + disposable += rxBus + .toObservable(EventPumpStatusChanged::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ updateGUI() }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventApexPumpDataChanged::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ updateGUI() }, fabricPrivacy::logException) + for (clazz in listOf( + EventInitializationChanged::class.java, + EventPumpStatusChanged::class.java, + EventApexPumpDataChanged::class.java, + EventPreferenceChange::class.java, + EventTempBasalChange::class.java, + EventQueueChanged::class.java, + )) { + disposable += rxBus + .toObservable(clazz) + .observeOn(aapsSchedulers.io) + .subscribe({ updateGUI() }, fabricPrivacy::logException) + } + + updateGUI() + handler.postDelayed(refreshLoop, T.mins(1).msecs()) + } + + override fun onPause() { + disposable.clear() + handler.removeCallbacks(refreshLoop) + super.onPause() + } + + private fun updateGUI() { + aapsLogger.error(LTag.UI, "updateGUI") + val status = pump.status + if (status == null) aapsLogger.error(LTag.UI, "No status available!") + + binding.connectionStatus.text = when { + activePlugin.activePump.isConnected() -> rh.gs(R.string.overview_connection_status_connected) + activePlugin.activePump.isConnecting() -> rh.gs(R.string.overview_connection_status_connecting) + else -> rh.gs(R.string.overview_connection_status_disconnected) + } + binding.serialNumber.text = pump.serialNumber + binding.pumpStatus.text = status?.getPumpStatus(rh) ?: "?" + binding.battery.text = status?.getBatteryLevel(rh) ?: "?" + binding.reservoir.text = status?.getReservoirLevel(rh) ?: "?" + binding.tempbasal.text = status?.getTBR(rh) ?: "?" + binding.baseBasalRate.text = status?.getBasal(rh) ?: "?" + binding.firmwareVersion.text = pump.firmwareVersion?.toLocalString(rh) ?: "?" + binding.lastBolus.text = pump.lastBolus?.toShortLocalString(rh) ?: "?" + } +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/ApexCrypto.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/ApexCrypto.kt new file mode 100644 index 000000000000..d3a9b4af0347 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/ApexCrypto.kt @@ -0,0 +1,22 @@ +package app.aaps.pump.apex.utils + +import app.aaps.core.utils.toHex +import kotlin.math.min + +class ApexCrypto { + companion object { + // CRC16-MODBUS + fun crc16(data: ByteArray, length: Int = Int.MAX_VALUE, offset: Int = 0): ByteArray { + var c = 0xFFFF + for (p in offset.. = List(48) { getBasalTimeFromMidnight(it * 30 * 60) } diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexBooleanKey.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexBooleanKey.kt new file mode 100644 index 000000000000..7ec687a9eb99 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexBooleanKey.kt @@ -0,0 +1,20 @@ +package app.aaps.pump.apex.utils.keys + +import app.aaps.core.keys.BooleanPreferenceKey + +enum class ApexBooleanKey( + override val key: String, + override val defaultValue: Boolean, + override val calculatedDefaultValue: Boolean = false, + override val engineeringModeOnly: Boolean = false, + override val defaultedBySM: Boolean = false, + override val showInApsMode: Boolean = true, + override val showInNsClientMode: Boolean = true, + override val showInPumpControlMode: Boolean = true, + override val dependency: BooleanPreferenceKey? = null, + override val negativeDependency: BooleanPreferenceKey? = null, + override val hideParentScreenIfHidden: Boolean = false +) : BooleanPreferenceKey { + LogInsulinChange("apex_log_insulin_change", true), + LogBatteryChange("apex_log_battery_change", true), +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexDoubleKey.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexDoubleKey.kt new file mode 100644 index 000000000000..ec4679590acd --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexDoubleKey.kt @@ -0,0 +1,23 @@ +package app.aaps.pump.apex.utils.keys + +import app.aaps.core.keys.BooleanPreferenceKey +import app.aaps.core.keys.DoublePreferenceKey + +enum class ApexDoubleKey( + override val key: String, + override val defaultValue: Double, + override val min: Double, + override val max: Double, + override val defaultedBySM: Boolean = false, + override val calculatedBySM: Boolean = false, + override val showInApsMode: Boolean = true, + override val showInNsClientMode: Boolean = true, + override val showInPumpControlMode: Boolean = true, + override val dependency: BooleanPreferenceKey? = null, + override val negativeDependency: BooleanPreferenceKey? = null, + override val hideParentScreenIfHidden: Boolean = false, +): DoublePreferenceKey { + // 0 == uninitialized + MaxBasal("apex_max_basal", 0.0, 0.0, 25.0), + MaxBolus("apex_max_bolus", 0.0, 0.0, 25.0), +} \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexStringKey.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexStringKey.kt new file mode 100644 index 000000000000..8a3bbb90edfa --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/utils/keys/ApexStringKey.kt @@ -0,0 +1,24 @@ +package app.aaps.pump.apex.utils.keys + +import app.aaps.core.keys.BooleanPreferenceKey +import app.aaps.core.keys.StringPreferenceKey +import app.aaps.pump.apex.connectivity.commands.pump.AlarmLength + +enum class ApexStringKey( + override val key: String, + override val defaultValue: String, + override val defaultedBySM: Boolean = false, + override val showInApsMode: Boolean = true, + override val showInNsClientMode: Boolean = true, + override val showInPumpControlMode: Boolean = true, + override val dependency: BooleanPreferenceKey? = null, + override val negativeDependency: BooleanPreferenceKey? = null, + override val hideParentScreenIfHidden: Boolean = false, + override val isPassword: Boolean = false, + override val isPin: Boolean = false +) : StringPreferenceKey { + SerialNumber("apex_serial_number", ""), + LastConnectedSerialNumber("apex_last_connected_serial_number", ""), + BluetoothAddress("apex_bt_address", ""), + AlarmSoundLength("apex_alarm_length", AlarmLength.Short.name) +} diff --git a/pump/apex/src/main/res/drawable/ic_apex.xml b/pump/apex/src/main/res/drawable/ic_apex.xml new file mode 100644 index 000000000000..b51b8546b033 --- /dev/null +++ b/pump/apex/src/main/res/drawable/ic_apex.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pump/apex/src/main/res/layout/apex_fragment.xml b/pump/apex/src/main/res/layout/apex_fragment.xml new file mode 100644 index 000000000000..a4a2aa55496b --- /dev/null +++ b/pump/apex/src/main/res/layout/apex_fragment.xml @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pump/apex/src/main/res/values/strings.xml b/pump/apex/src/main/res/values/strings.xml new file mode 100644 index 000000000000..09e22ee4ec70 --- /dev/null +++ b/pump/apex/src/main/res/values/strings.xml @@ -0,0 +1,86 @@ + + + APEX TruCare III + APEX + Native integration with APEX TruCare III insulin pump + APEX Pump Settings + + Alarm length + Short + Medium + Long + + Maximum bolus [U] + Maximum basal rate [U/h] + + Pump serial number + + Disconnected + Connecting + Connected + + Normal + Suspended + Alarm + + %1$d%% + %1$d%% (%2$d mV) + %1$.3fU + %1$.3fU (ends %2$d:%3$d) + %1$.3fU (%2$d min left) + %1$d%% (%2$d min left) + %1$.3fU (%2$d h %3$d min left) + %1$d%% (%2$d h %3$d min left) + %1$.3fU (%2$d min ago) + %1$.3fU (%2$d h ago) + v%1$d.%2$d (v%3$d.%4$d) + + Pump status + BT status + Last bolus + Insulin + Battery + TBR + Base basal + + Current action + Idle + Updating pump status + Updating TDDs + Updating alarms + Getting basal profiles + Getting version + Updating boluses + Setting bolus %1$.3fU + + Pump is suspended + Pump has unsupported firmware + + Occlusion + Low battery + Low reservoir level + Battery is dead + Reservoir is empty + Hardware error ($1%s) + Unknown error $1%s, report to developers + Unknown error + + Delivering %1$.3fU + Delivered %1$.3fU successfully + Bolus was cancelled + + Pump is not ready + Failed to switch basal profile index + Failed to update basal profile + Pump unreachable + Pump is suspended + Failed to start bolus + Failed to cancel TBR + Failed to set TBR + Only absolute TBRs are supported + Firmware version + Connection status + Pump status + GET STATUS + GET BOLUS + diff --git a/pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt b/pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt new file mode 100644 index 000000000000..75f07bd81bc9 --- /dev/null +++ b/pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt @@ -0,0 +1,144 @@ +package app.aaps.pump.apex + +import app.aaps.pump.apex.connectivity.commands.device.Bolus +import app.aaps.pump.apex.connectivity.commands.pump.AlarmType +import app.aaps.pump.apex.connectivity.commands.pump.BasalProfile +import app.aaps.pump.apex.connectivity.commands.pump.BolusDeliverySpeed +import app.aaps.pump.apex.connectivity.commands.pump.BolusEntry +import app.aaps.pump.apex.connectivity.commands.pump.CommandResponse +import app.aaps.pump.apex.connectivity.commands.pump.PumpCommand +import app.aaps.pump.apex.connectivity.commands.pump.PumpObject +import app.aaps.pump.apex.connectivity.commands.pump.ScreenBrightness +import app.aaps.pump.apex.connectivity.commands.pump.StatusV1 +import app.aaps.pump.apex.connectivity.commands.pump.TDDEntry +import app.aaps.pump.apex.connectivity.commands.pump.Version +import app.aaps.pump.apex.interfaces.ApexDeviceInfo +import app.aaps.shared.tests.TestBase +import org.joda.time.DateTime +import org.junit.jupiter.api.Test + +class CommandsTest : TestBase() { + private val info = object : ApexDeviceInfo { + override var serialNumber = "12345678" + } + + private fun deserialiseHead(expectedType: PumpObject, data: ByteArray): PumpCommand { + val command = PumpCommand(data) + assert(command.isCompleteCommand()) + assert(command.verify()) + assert(PumpObject.findObject(command.id!!, command.objectType, command.objectData) == expectedType) + return command + } + + @Test + fun pump_commandResponse() { + val command = deserialiseHead(PumpObject.CommandResponse, ubyteArrayOf(0xaau, 0x0au, 0x00u, 0xa1u, 0xa0u, 0xaau, 0x0cu, 0x00u, 0xdau, 0xf5u).toByteArray()) + val data = CommandResponse(command) + assert(data.code == CommandResponse.Code.StandardBolusProgress) + assert(data.dose == 12) + + assert(command.trailing == null) + } + + @Test + fun pump_bolusEntry() { + val command = deserialiseHead(PumpObject.BolusEntry, ubyteArrayOf(0xaau, 0x16u, 0x80u, 0xa3u, 0x21u, 0x00u, 0x25u, 0x01u, 0x25u, 0x17u, 0x52u, 0x59u, 0x09u, 0x00u, 0x09u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x78u).toByteArray()) + val data = BolusEntry(command) + assert(data.dateTime == DateTime(2025, 1, 25, 17, 52, 59)) + assert(data.extendedDose == 0) + assert(data.standardDose == 9) + + assert(command.trailing == null) + } + + @Test + fun pump_tddEntry() { + val command = deserialiseHead(PumpObject.TDDEntry, ubyteArrayOf(0xaau, 0x12u, 0x5cu, 0xa3u, 0x06u, 0x00u, 0x88u, 0x00u, 0x56u, 0x01u, 0x0cu, 0x00u, 0x25u, 0x01u, 0x26u, 0x00u, 0x50u, 0xa0u, 0xaau, 0x12u, 0x5cu, 0xa3u, 0x06u, 0x00u, 0x88u, 0x00u, 0x56u, 0x01u, 0x0cu, 0x00u, 0x25u, 0x01u, 0x26u, 0x00u, 0x50u, 0xa0u).toByteArray()) + val data = TDDEntry(command) + assert(data.dateTime == DateTime(2025, 1, 26, 0, 0)) + assert(data.bolus == 136) { data.bolus } + assert(data.basal == 342) { data.basal } + assert(data.temporaryBasal == 12) { data.temporaryBasal } + + val trailing = command.trailing + assert(trailing != null) + assert(trailing!!.isCompleteCommand()) + assert(trailing.verify()) + assert(PumpObject.findObject(trailing.id!!, trailing.objectType, trailing.objectData) == PumpObject.TDDEntry) + } + + @Test + fun pump_basalPattern() { + val command = deserialiseHead(PumpObject.BasalProfile, ubyteArrayOf(0xaau, 0x68u, 0x08u, 0xa3u, 0x08u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x27u, 0x00u, 0x27u, 0x00u, 0x27u, 0x00u, 0x27u, 0x00u, 0x28u, 0x00u, 0x28u, 0x00u, 0x28u, 0x00u, 0x28u, 0x00u, 0x26u, 0x00u, 0x26u, 0x00u, 0x26u, 0x00u, 0x26u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x23u, 0x00u, 0x23u, 0x00u, 0x22u, 0x00u, 0x22u, 0x00u, 0x22u, 0x00u, 0x22u, 0x00u, 0x22u, 0x00u, 0x22u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x24u, 0x00u, 0x40u, 0x6au).toByteArray()) + val data = BasalProfile(command) + val rates = listOf( + 36, 36, + 39, 39, + 39, 39, + 40, 40, + 40, 40, + 38, 38, + 38, 38, + 36, 36, + 35, 35, + 34, 34, + 34, 34, + 34, 34, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + 36, 36, + ) + assert(data.rates == rates) + assert(data.index == 0) + } + + @Test + fun pump_statusV1() { + val command = deserialiseHead(PumpObject.StatusV1, ubyteArrayOf(0xaau, 0x60u, 0x01u, 0xa3u, 0x00u, 0xaau, 0x04u, 0x01u, 0x00u, 0x00u, 0x06u, 0x01u, 0x00u, 0x01u, 0x14u, 0x04u, 0x00u, 0x00u, 0x00u, 0x00u, 0x2cu, 0x01u, 0xb7u, 0x05u, 0x00u, 0x00u, 0x96u, 0x00u, 0x00u, 0x00u, 0xa0u, 0x00u, 0x0cu, 0x03u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x19u, 0x01u, 0x1cu, 0x15u, 0x0eu, 0x00u, 0x00u, 0x00u, 0x15u, 0x35u, 0x01u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x24u, 0x00u, 0x0du, 0x0eu, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0xc5u, 0xb3u).toByteArray()) + val data = StatusV1(command) + assert(data.batteryLevel!!.approximatePercentage == 100) + assert(data.alarmType == AlarmType.Vibration) + assert(data.deliverySpeed == BolusDeliverySpeed.Standard) + assert(data.brightness == ScreenBrightness.P10) + assert(data.keyboardLockEnabled) + assert(!data.autoSuspendEnabled) + assert(data.autoSuspendDuration == 1) + assert(data.lowReservoirThreshold == 20) + assert(data.lowReservoirTimeLeftThreshold == 4) + assert(!data.totalDailyDoseLimitEnabled) + assert(data.screenTimeout == 300) + assert(data.currentBasalRate == 36) + } + + @Test + fun pump_heartbeat() { + deserialiseHead(PumpObject.Heartbeat, ubyteArrayOf(0xaau, 0x06u, 0x00u, 0xa5u, 0x01u, 0x00u, 0x81u, 0xa2u).toByteArray()) + } + + @Test + fun pump_version() { + val command = deserialiseHead(PumpObject.FirmwareEntry, ubyteArrayOf(0xaau, 0x10u, 0x00u, 0xa3u, 0x31u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x06u, 0x19u, 0x04u, 0x0au, 0x30u, 0xcfu).toByteArray()) + val data = Version(command) + + assert(data.firmwareMajor == 6) + assert(data.firmwareMinor == 25) + assert(data.protocolMajor == 4) + assert(data.protocolMinor == 10) + } + + @Test + fun device_bolus() { + val expected = ubyteArrayOf(0x35u, 0x17u, 0x00u, 0xa1u, 0x12u, 0xaau, 0x41u, 0x50u, 0x45u, 0x58u, 0x31u, 0x32u, 0x33u, 0x34u, 0x35u, 0x36u, 0x37u, 0x38u, 0x3cu, 0x00u, 0x00u, 0x6fu, 0x65u).toByteArray() + val command = Bolus(info, 60) + assert(expected.contentEquals(command.serialize())) + } +} diff --git a/settings.gradle b/settings.gradle index 8de57d1b7c8d..8b295e2e0d53 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,6 +24,7 @@ include ':plugins:sensitivity' include ':plugins:smoothing' include ':plugins:source' include ':plugins:sync' +include ':pump:apex' include ':pump:combov2' include ':pump:combov2:comboctl' include ':pump:dana' From 57ea58ee8ec513ebedfdf100c0d45449f2465605 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Wed, 19 Feb 2025 19:18:53 +0300 Subject: [PATCH 02/29] apex: fix TBR and add pump icon to the fragmentFix TBR and add pump icon to the fragment --- .../kotlin/app/aaps/pump/apex/ApexService.kt | 27 +++++++++++++------ .../src/main/res/layout/apex_fragment.xml | 9 +++++++ pump/apex/src/main/res/values/strings.xml | 1 + 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 7ee550155efa..564d454f12ee 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -144,6 +144,8 @@ class ApexService: DaggerService(), ApexBluetoothCallback { private var lastBolusDateTime = DateTime(0) private var lastConnectedTimestamp = System.currentTimeMillis() + private var manualDisconnect = false + val lastConnected: Long get() = if (connectionStatus != ApexBluetooth.Status.CONNECTED) { lastConnectedTimestamp @@ -340,19 +342,19 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } val doseRaw = (dose / 0.025).toInt() - if (dose % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounded down.") + if (dose % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[temporaryBasal caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounded down.") val durationRaw = durationMinutes / 15 - if (durationMinutes % 15 > 0) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus duration is not aligned to 15 minute steps! Rounded down.") + if (durationMinutes % 15 > 0) aapsLogger.warn(LTag.PUMPCOMM, "[temporaryBasal caller=$caller] Bolus duration is not aligned to 15 minute steps! Rounded down.") - val response = executeWithResponse(TemporaryBasal(apexDeviceInfo, true, doseRaw, durationRaw)) + val response = executeWithResponse(TemporaryBasal(apexDeviceInfo, true, durationRaw, doseRaw)) if (response == null) { - aapsLogger.error(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Timed out while trying to communicate with the pump") + aapsLogger.error(LTag.PUMPCOMM, "[temporaryBasal caller=$caller] Timed out while trying to communicate with the pump") return false } if (response.code != CommandResponse.Code.Accepted) { - aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to begin extended bolus: ${response.code.name}") + aapsLogger.error(LTag.PUMPCOMM, "[caller=$caller] Failed to start temporary basal: ${response.code.name}") return false } @@ -856,10 +858,13 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } fun startConnection() { - if (apexDeviceInfo.serialNumber.isNotEmpty()) apexBluetooth.connect() + if (apexDeviceInfo.serialNumber.isEmpty()) return + manualDisconnect = false + apexBluetooth.connect() } fun disconnect() { + manualDisconnect = true if (apexBluetooth.status != ApexBluetooth.Status.DISCONNECTED) apexBluetooth.disconnect() } @@ -911,13 +916,19 @@ class ApexService: DaggerService(), ApexBluetoothCallback { if (isDisconnectLoopRunning) return isDisconnectLoopRunning = true Thread { - while (connectionStatus != ApexBluetooth.Status.CONNECTED) { + while (connectionStatus != ApexBluetooth.Status.CONNECTED && !manualDisconnect) { if (connectionStatus == ApexBluetooth.Status.DISCONNECTED) { aapsLogger.debug(LTag.PUMPCOMM, "Starting connection loop") startConnection() } SystemClock.sleep(250) } + + if (manualDisconnect) { + aapsLogger.debug(LTag.PUMPCOMM, "Manual disconnect detected!") + disconnect() + } + aapsLogger.debug(LTag.PUMPCOMM, "Exiting") isDisconnectLoopRunning = false }.start() @@ -945,7 +956,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { aapsLogger.error(LTag.PUMP, "Pump unreachable!") } - spawnLoop() + if (!manualDisconnect) spawnLoop() pump.gettingReady = true }.start() diff --git a/pump/apex/src/main/res/layout/apex_fragment.xml b/pump/apex/src/main/res/layout/apex_fragment.xml index a4a2aa55496b..76cba50dd467 100644 --- a/pump/apex/src/main/res/layout/apex_fragment.xml +++ b/pump/apex/src/main/res/layout/apex_fragment.xml @@ -401,4 +401,13 @@ + + \ No newline at end of file diff --git a/pump/apex/src/main/res/values/strings.xml b/pump/apex/src/main/res/values/strings.xml index 09e22ee4ec70..fd64af294f12 100644 --- a/pump/apex/src/main/res/values/strings.xml +++ b/pump/apex/src/main/res/values/strings.xml @@ -83,4 +83,5 @@ Pump status GET STATUS GET BOLUS + Suspend From 77bc79168f2347ce3f2d8c29d4f5c2cfa7400fcf Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Fri, 21 Feb 2025 13:28:52 +0300 Subject: [PATCH 03/29] Apex: bug fixes --- .../app/aaps/pump/apex/ApexPumpPlugin.kt | 2 +- .../kotlin/app/aaps/pump/apex/ApexService.kt | 95 +++++++++++++------ .../connectivity/commands/pump/PumpCommand.kt | 3 + .../connectivity/commands/pump/PumpObjects.kt | 30 +++--- pump/apex/src/main/res/values/strings.xml | 8 +- .../kotlin/app/aaps/pump/apex/CommandsTest.kt | 5 +- 6 files changed, 94 insertions(+), 49 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index fa8a9e7bc669..a2ff47ba626a 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -128,7 +128,6 @@ class ApexPumpPlugin @Inject constructor( } override val isFakingTempsByExtendedBoluses = false - override fun isBusy() = false override fun canHandleDST() = false override fun manufacturer() = ManufacturerType.Apex @@ -143,6 +142,7 @@ class ApexPumpPlugin @Inject constructor( get() = pump.batteryLevel.percentage override val pumpDescription = PumpDescription().fillFor(model()) + override fun isBusy() = service?.isBusy ?: false override fun isSuspended() = pump.isSuspended override fun isInitialized() = !pump.gettingReady && pump.status != null override fun isConnecting() = service?.connectionStatus == ApexBluetooth.Status.CONNECTING diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 564d454f12ee..7cbbcc915010 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -13,6 +13,7 @@ import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.pump.PumpSync +import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.AapsSchedulers @@ -20,6 +21,7 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventDismissNotification import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress import app.aaps.core.interfaces.rx.events.EventPreferenceChange +import app.aaps.core.interfaces.rx.events.EventProfileSwitchChanged import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.fabric.FabricPrivacy @@ -69,6 +71,7 @@ import java.util.TimerTask import javax.inject.Inject import kotlin.concurrent.schedule import kotlin.math.abs +import kotlin.math.roundToInt /** * @author Roman Rikhter (teledurak@gmail.com) @@ -146,14 +149,19 @@ class ApexService: DaggerService(), ApexBluetoothCallback { private var manualDisconnect = false + val isBusy: Boolean + get() = commandLock.isLocked + val lastConnected: Long get() = if (connectionStatus != ApexBluetooth.Status.CONNECTED) { lastConnectedTimestamp } else System.currentTimeMillis() fun getValue(value: GetValue.Value): List? = synchronized(commandLock) { synchronized(getValueResult) { - if (connectionStatus != ApexBluetooth.Status.CONNECTED) return null - aapsLogger.debug(LTag.PUMPCOMM, "Executing GetValue(${value.name})") + if (connectionStatus != ApexBluetooth.Status.CONNECTED) { + aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Error - pump is disconnected") + return null + } getValueResult.clear() getValueResult.targetObject = when (value) { @@ -173,29 +181,36 @@ class ApexService: DaggerService(), ApexBluetoothCallback { apexBluetooth.send(GetValue(apexDeviceInfo, value)) try { - aapsLogger.debug(LTag.PUMPCOMM, "${value.name} | Waiting for response") - getValueResult.waitMillis(if (getValueResult.isSingleObject) 5000 else 60000) + aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Waiting for response") + getValueResult.waitMillis(if (getValueResult.isSingleObject) 1000 else 5000) } catch (e: InterruptedException) { - aapsLogger.error(LTag.PUMPCOMM, "getValue InterruptedException", e) + aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") + isGetThreadRunning = false + return null } + aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Completed") getValueResult.response }} private fun executeWithResponse(command: DeviceCommand): CommandResponse? = synchronized(commandLock) { synchronized(commandResponse) { - if (connectionStatus != ApexBluetooth.Status.CONNECTED) return null + if (connectionStatus != ApexBluetooth.Status.CONNECTED) { + aapsLogger.debug(LTag.PUMPCOMM, "$command | Error - pump is disconnected") + return null + } - aapsLogger.debug(LTag.PUMPCOMM, "Executing $command") commandResponse.clear() - apexBluetooth.send(command) try { aapsLogger.debug(LTag.PUMPCOMM, "$command | Waiting for response") - commandResponse.waitMillis(5000) + commandResponse.waitMillis(1000) } catch (e: InterruptedException) { - aapsLogger.error(LTag.PUMPCOMM, "executeWithResponse InterruptedException", e) + aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") + commandResponse.waiting = false + return null } + aapsLogger.debug(LTag.PUMPCOMM, "$command | Completed") commandResponse.response }} @@ -272,8 +287,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { return false } - val doseRaw = (dbi.insulin / 0.025).toInt() - if (dbi.insulin % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[bolus caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounding down.") + val doseRaw = (dbi.insulin / 0.025).roundToInt() val response = executeWithResponse(Bolus(apexDeviceInfo, doseRaw)) if (response == null) { @@ -309,13 +323,13 @@ class ApexService: DaggerService(), ApexBluetoothCallback { ) _bolusCompletable = CompletableDeferred() + getStatus("ApexService-bolus") return true } fun extendedBolus(dose: Double, durationMinutes: Int, caller: String): Boolean { aapsLogger.debug(LTag.PUMPCOMM, "extendedBolus - $caller") - val doseRaw = (dose / 0.025).toInt() - if (dose % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounded down.") + val doseRaw = (dose / 0.025).roundToInt() val durationRaw = durationMinutes / 15 if (durationMinutes % 15 > 0) aapsLogger.warn(LTag.PUMPCOMM, "[extendedBolus caller=$caller] Bolus duration is not aligned to 15 minute steps! Rounded down.") @@ -341,8 +355,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { return false } - val doseRaw = (dose / 0.025).toInt() - if (dose % 0.025 > 0.001) aapsLogger.warn(LTag.PUMPCOMM, "[temporaryBasal caller=$caller] Bolus dose is not aligned to 0.025U steps! Rounded down.") + val doseRaw = (dose / 0.025).roundToInt() val durationRaw = durationMinutes / 15 if (durationMinutes % 15 > 0) aapsLogger.warn(LTag.PUMPCOMM, "[temporaryBasal caller=$caller] Bolus duration is not aligned to 15 minute steps! Rounded down.") @@ -371,6 +384,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { ) aapsLogger.debug(LTag.PUMP, "Started TBR ${dose}U for ${durationMinutes}min by $caller") + getStatus("ApexService-temporaryBasal") return true } @@ -388,6 +402,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } onBolusFailed(true) + getStatus("ApexService-cancelBolus") return true } @@ -412,6 +427,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { pumpSerial = apexDeviceInfo.serialNumber, ) + getStatus("ApexService-cancelTBR") return true } @@ -422,8 +438,8 @@ class ApexService: DaggerService(), ApexBluetoothCallback { pump.lastV1!!.toUpdateSettingsV1( apexDeviceInfo, AlarmLength.valueOf(preferences.get(ApexStringKey.AlarmSoundLength)), - maxSingleBolus = (preferences.get(ApexDoubleKey.MaxBolus) / 0.025).toInt(), - maxBasalRate = (preferences.get(ApexDoubleKey.MaxBasal) / 0.025).toInt(), + maxSingleBolus = (preferences.get(ApexDoubleKey.MaxBolus) / 0.025).roundToInt(), + maxBasalRate = (preferences.get(ApexDoubleKey.MaxBasal) / 0.025).roundToInt(), enableAdvancedBolus = false, ) else @@ -471,6 +487,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { return false } + if (!commandQueue.isRunning(Command.CommandType.BASAL_PROFILE)) rxBus.send(EventProfileSwitchChanged()) return true } @@ -481,7 +498,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { val response = executeWithResponse(UpdateBasalProfileRates( apexDeviceInfo, - doses.map { (it / 0.025).toInt() } + doses.map { (it / 0.025).roundToInt() } )) if (response == null) { aapsLogger.error(LTag.PUMPCOMM, "[updateBasalPatternIndex caller=$caller] Timed out while trying to communicate with the pump") @@ -575,7 +592,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { rxBus.send(EventOverviewBolusProgress.apply { t = bolus.treatment - percent = (bolus.currentDose / bolus.requestedDose * 100).toInt() + percent = (bolus.currentDose / bolus.requestedDose * 100).roundToInt() status = rh.gs(R.string.status_delivering, dose) }) } @@ -584,6 +601,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { aapsLogger.debug(LTag.PUMPCOMM, "bolus completed") if (pump.inProgressBolus == null) return pump.inProgressBolus!!.currentDose = dose + waitingForCurrentBolusInHistory = true rxBus.send(EventOverviewBolusProgress.apply { percent = 100 @@ -606,6 +624,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } if (pump.inProgressBolus!!.currentDose >= 0.025) { + waitingForCurrentBolusInHistory = true // Request new bolus history to fixup bolus ID and delivered amount. getBoluses("ApexService-onBolusCompleted") } else { @@ -775,7 +794,9 @@ class ApexService: DaggerService(), ApexBluetoothCallback { private fun onHeartbeat() { aapsLogger.debug(LTag.PUMPCOMM, "Got heartbeat") - if (pump.gettingReady) return + + // Pump sent heartbeat => connection is established. + pump.gettingReady = false if (!getStatus("HeartbeatHandler")) return if (!getBoluses("HeartbeatHandler")) return @@ -820,6 +841,8 @@ class ApexService: DaggerService(), ApexBluetoothCallback { waitingForCurrentBolusInHistory = false _bolusCompletable?.complete(ipb) _bolusCompletable = null + + getStatus("ApexService-updateAfterBolus") return } @@ -937,13 +960,18 @@ class ApexService: DaggerService(), ApexBluetoothCallback { override fun onDisconnect() = Thread { aapsLogger.debug(LTag.PUMPCOMM, "onDisconnect") getValueResult.clear() + + isGetThreadRunning = false synchronized(getValueResult) { + getValueResult.waiting = false getValueResult.notifyAll() } - commandResponse.clear() synchronized(commandResponse) { + commandResponse.waiting = false commandResponse.notifyAll() } + + lastConnectedTimestamp = System.currentTimeMillis() if (unreachableTimerTask == null) @@ -957,7 +985,6 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } if (!manualDisconnect) spawnLoop() - pump.gettingReady = true }.start() private var isGetThreadRunning = false @@ -966,9 +993,12 @@ class ApexService: DaggerService(), ApexBluetoothCallback { aapsLogger.error(LTag.PUMPCOMM, "Invalid command with crc ${command.checksum}") return@Thread } - val type = PumpObject.findObject(command.id!!, command.objectType, command.objectData) + val type = PumpObject.findObject(command.id!!, command.objectData, aapsLogger) aapsLogger.debug(LTag.PUMPCOMM, "from PUMP: ${command.id!!.name}, ${type?.name}") + if (type == null) return@Thread + + notifyAboutResponse(command, type) when (type) { PumpObject.CommandResponse -> onCommandResponse(CommandResponse(command)) PumpObject.StatusV1 -> onStatusV1(StatusV1(command)) @@ -977,9 +1007,11 @@ class ApexService: DaggerService(), ApexBluetoothCallback { PumpObject.TDDEntry -> onTDDEntry(TDDEntry(command)) else -> {} } + }.start() - if (!getValueResult.waiting) return@Thread - if (type != getValueResult.targetObject) return@Thread + private fun notifyAboutResponse(command: PumpCommand, type: PumpObject) { + if (!getValueResult.waiting) return + if (type != getValueResult.targetObject) return getValueResult.add( when (type) { @@ -991,7 +1023,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { PumpObject.TDDEntry -> TDDEntry(command) PumpObject.BolusEntry -> BolusEntry(command) PumpObject.FirmwareEntry -> Version(command) - else -> return@Thread + else -> return } ) @@ -1005,13 +1037,14 @@ class ApexService: DaggerService(), ApexBluetoothCallback { getValueLastTaskTimestamp = System.currentTimeMillis() runGetThread() } - }.start() + } private fun runGetThread() { if (isGetThreadRunning) return + aapsLogger.debug(LTag.PUMPCOMM, "Running GET thread") isGetThreadRunning = true Thread { - while (true) { + while (isGetThreadRunning) { val now = System.currentTimeMillis() if (now - getValueLastTaskTimestamp >= 500) { break @@ -1020,6 +1053,10 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } SystemClock.sleep(250) } + if (!isGetThreadRunning) { + aapsLogger.debug(LTag.PUMPCOMM, "GET thread killed") + return@Thread + } isGetThreadRunning = false aapsLogger.debug(LTag.PUMPCOMM, "Chunked response has completed") diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt index a64f587da944..f9e19a97a892 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpCommand.kt @@ -2,6 +2,7 @@ package app.aaps.pump.apex.connectivity.commands.pump import app.aaps.pump.apex.connectivity.commands.CommandId import androidx.annotation.VisibleForTesting +import app.aaps.core.utils.toHex import app.aaps.pump.apex.utils.ApexCrypto // Read-only commands which we get from the pump. @@ -60,4 +61,6 @@ class PumpCommand(private var data: ByteArray) { data += remainingData return isCompleteCommand() } + + override fun toString(): String = "PumpCommand(type=0x${type.toString(16)}, objType=0x${objectType.toString(16)}, data=${objectData.toHex()})" } \ No newline at end of file diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt index f706c30df9a7..726460e07891 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt @@ -1,32 +1,36 @@ package app.aaps.pump.apex.connectivity.commands.pump +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag import app.aaps.pump.apex.connectivity.commands.CommandId enum class PumpObject( val commandId: CommandId = CommandId.GetValue, - val objectId: Int? = null, - val valueId: Int? = null, + //val objectId: Int? = null, + val valueId: List? = null, ) { Heartbeat(commandId = CommandId.Heartbeat), CommandResponse(commandId = CommandId.SetValue), - StatusV1(objectId = 0x01, valueId = 0x00), - WizardStatus(objectId = 0x01, valueId = 0x07), - BasalProfile(objectId = 0x08), - AlarmEntry(objectId = 0x14), - TDDEntry(objectId = 0x5c), - BolusEntry(objectId = 0x80), - FirmwareEntry(objectId = 0x00, valueId = 0x31); + StatusV1(valueId = listOf(0x00)), + WizardStatus(valueId = listOf(0x07)), + BasalProfile(valueId = listOf(0x08)), + AlarmEntry(valueId = listOf(0x03)), + TDDEntry(valueId = listOf(0x06)), + BolusEntry(valueId = listOf(0x21, 0x01)), + FirmwareEntry(valueId = listOf(0x31)); companion object { - fun findObject(commandId: CommandId, objectId: Int, objectData: ByteArray): PumpObject? { + fun findObject(commandId: CommandId, objectData: ByteArray, aapsLogger: AAPSLogger? = null): PumpObject? { + val valueId = objectData[0].toInt() for (e in entries) { if (commandId != e.commandId) continue - if (e.objectId == null) return e - if (objectId != e.objectId) continue + //if (e.objectId == null) return e + //if (objectId != e.objectId) continue if (e.valueId == null) return e - if (objectData[0].toInt() != e.valueId) continue + if (!e.valueId.contains(valueId)) continue return e } + aapsLogger?.debug(LTag.PUMPBTCOMM, "Object [0x${commandId.name}:0x${valueId.toString(16)}] not found") return null } } diff --git a/pump/apex/src/main/res/values/strings.xml b/pump/apex/src/main/res/values/strings.xml index fd64af294f12..9b58eaad9d16 100644 --- a/pump/apex/src/main/res/values/strings.xml +++ b/pump/apex/src/main/res/values/strings.xml @@ -25,11 +25,11 @@ %1$d%% %1$d%% (%2$d mV) - %1$.3fU - %1$.3fU (ends %2$d:%3$d) - %1$.3fU (%2$d min left) + %1$.3fU/h + %1$.3fU/h + %1$.3fU/h (%2$d min left) %1$d%% (%2$d min left) - %1$.3fU (%2$d h %3$d min left) + %1$.3fU/h (%2$d h %3$d min left) %1$d%% (%2$d h %3$d min left) %1$.3fU (%2$d min ago) %1$.3fU (%2$d h ago) diff --git a/pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt b/pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt index 75f07bd81bc9..511cfadd44fd 100644 --- a/pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt +++ b/pump/apex/src/test/kotlin/app/aaps/pump/apex/CommandsTest.kt @@ -26,7 +26,8 @@ class CommandsTest : TestBase() { val command = PumpCommand(data) assert(command.isCompleteCommand()) assert(command.verify()) - assert(PumpObject.findObject(command.id!!, command.objectType, command.objectData) == expectedType) + assert(PumpObject.findObject(command.id!!, command.objectData) == expectedType) + println("Processed $command") return command } @@ -64,7 +65,7 @@ class CommandsTest : TestBase() { assert(trailing != null) assert(trailing!!.isCompleteCommand()) assert(trailing.verify()) - assert(PumpObject.findObject(trailing.id!!, trailing.objectType, trailing.objectData) == PumpObject.TDDEntry) + assert(PumpObject.findObject(trailing.id!!, trailing.objectData) == PumpObject.TDDEntry) } @Test From 562b4fa58b8e38ad57297a320fb3d601964c3910 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:05:59 +0300 Subject: [PATCH 04/29] apex: remove extended bolus capability Pump doesn't support doing standard boluses while extended bolus is running. I don't see a reason to implement such extended bolus functionality. --- .../main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt index c4bc5bf6f2db..53f6548f8435 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt @@ -20,7 +20,7 @@ enum class PumpCapability { DiaconnCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.Refill, Capability.ReplaceBattery, Capability.TDD, Capability.ManualTDDLoad)), // EopatchCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min)), MedtrumCapabilities(arrayOf(Capability.Bolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min, Capability.TDD)), // Technically the pump supports ExtendedBolus, but not implemented (yet) - ApexCapabilities(arrayOf(Capability.Bolus, Capability.ExtendedBolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min, Capability.TDD, Capability.ManualTDDLoad, Capability.ReplaceBattery, Capability.Refill)) + ApexCapabilities(arrayOf(Capability.Bolus, Capability.TempBasal, Capability.BasalProfileSet, Capability.BasalRate30min, Capability.TDD, Capability.ManualTDDLoad, Capability.ReplaceBattery, Capability.Refill)) ; var children: ArrayList = ArrayList() From 680def9242e8e4d0dc32c698475b4dacb11f538d Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:08:17 +0300 Subject: [PATCH 05/29] constraints: Respect pump bolus and basal dynamic constraints Some pumps have configurable max bolus and max basal values, but SafetyPlugin didn't respect that. This change fixes this issue. --- .../app/aaps/core/data/pump/defs/PumpDescription.kt | 2 ++ .../aaps/plugins/constraints/safety/SafetyPlugin.kt | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt index fb0463d43bc1..0720265a5786 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt @@ -25,6 +25,7 @@ open class PumpDescription { var basalMaximumRate = 0.0 var isRefillingCapable = false var isBatteryReplaceable = false + var maxBolusSize = 0.0 //var storesCarbInfo = false var is30minBasalRatesCapable = false @@ -64,6 +65,7 @@ open class PumpDescription { needsManualTDDLoad = true hasCustomUnreachableAlertCheck = false useHardwareLink = false + maxBolusSize = 0.0 } companion object { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt index 4cbc3eb373e0..f76b750387c6 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt @@ -110,6 +110,10 @@ class SafetyPlugin @Inject constructor( if (pump.pumpDescription.tempBasalStyle == PumpDescription.ABSOLUTE) { val pumpLimit = pump.pumpDescription.pumpType.tbrSettings()?.maxDose ?: 0.0 absoluteRate.setIfSmaller(pumpLimit, rh.gs(app.aaps.core.ui.R.string.limitingbasalratio, pumpLimit, rh.gs(app.aaps.core.ui.R.string.pumplimit)), this) + + // Not all pumps have dynamic TBR constraint + val dynamicPumpLimit = pump.pumpDescription.maxTempAbsolute + if (dynamicPumpLimit > 0.0) absoluteRate.setIfSmaller(dynamicPumpLimit, rh.gs(app.aaps.core.ui.R.string.limitingbasalratio, dynamicPumpLimit, rh.gs(app.aaps.core.ui.R.string.pumplimit)), this) } // do rounding @@ -149,6 +153,12 @@ class SafetyPlugin @Inject constructor( insulin.setIfSmaller(maxBolus, rh.gs(app.aaps.core.ui.R.string.limitingbolus, maxBolus, rh.gs(R.string.maxvalueinpreferences)), this) insulin.setIfSmaller(hardLimits.maxBolus(), rh.gs(app.aaps.core.ui.R.string.limitingbolus, hardLimits.maxBolus(), rh.gs(R.string.hardlimit)), this) val pump = activePlugin.activePump + val dynamicPumpLimit = pump.pumpDescription.maxBolusSize + if (dynamicPumpLimit > 0.0) { + // Not all pumps have dynamic bolus size constraint + insulin.setIfSmaller(dynamicPumpLimit, rh.gs(app.aaps.core.ui.R.string.limitingbolus, dynamicPumpLimit, rh.gs(app.aaps.core.ui.R.string.pumplimit)), this) + } + val rounded = pump.pumpDescription.pumpType.determineCorrectBolusSize(insulin.value()) insulin.setIfDifferent(rounded, rh.gs(app.aaps.core.ui.R.string.pumplimit), this) return insulin From d84524ebaa42d4da4b77ab37921dd9a7b6748dc2 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:08:49 +0300 Subject: [PATCH 06/29] apex: fix reservoir value text --- pump/apex/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pump/apex/src/main/res/values/strings.xml b/pump/apex/src/main/res/values/strings.xml index 9b58eaad9d16..284b073833da 100644 --- a/pump/apex/src/main/res/values/strings.xml +++ b/pump/apex/src/main/res/values/strings.xml @@ -25,7 +25,7 @@ %1$d%% %1$d%% (%2$d mV) - %1$.3fU/h + %1$.3fU %1$.3fU/h %1$.3fU/h (%2$d min left) %1$d%% (%2$d min left) From 372a76214ca43089874f02ccf2cbc0faa5cb9fdf Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:09:44 +0300 Subject: [PATCH 07/29] apex: add reservoir value to short status --- pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index a2ff47ba626a..d55e10b63404 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -217,6 +217,7 @@ class ApexPumpPlugin @Inject constructor( "${rh.gs(R.string.status_last_bolus)}: ${pump.lastBolus?.toShortLocalString(rh) ?: "-"}\n" + "${rh.gs(R.string.status_tbr)}: ${status.getTBR(rh)}\n" + "${rh.gs(R.string.status_basal)}: ${status.getBasal(rh)}\n" + + "${rh.gs(R.string.status_reservoir)}: ${status.getReservoirLevel(rh)}\n" + "${rh.gs(R.string.status_battery)}: ${status.getBatteryLevel(rh)}" aapsLogger.debug(LTag.PUMP, "Short status: $ret") From 9c7c80c81407ac2373e420e4a629d9abc5dae645 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:09:57 +0300 Subject: [PATCH 08/29] apex: do not report pump as busy --- pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index d55e10b63404..35b37e7c88b6 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -142,7 +142,7 @@ class ApexPumpPlugin @Inject constructor( get() = pump.batteryLevel.percentage override val pumpDescription = PumpDescription().fillFor(model()) - override fun isBusy() = service?.isBusy ?: false + override fun isBusy() = false //service?.isBusy ?: false override fun isSuspended() = pump.isSuspended override fun isInitialized() = !pump.gettingReady && pump.status != null override fun isConnecting() = service?.connectionStatus == ApexBluetooth.Status.CONNECTING From ad196bd8f5af92203cbb3df6cd1ae0003c75b00e Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:10:07 +0300 Subject: [PATCH 09/29] apex: report max bolus size --- pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index 35b37e7c88b6..45550da842c6 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -228,6 +228,7 @@ class ApexPumpPlugin @Inject constructor( aapsLogger.debug(LTag.PUMP, "Updating pump description") pumpDescription.maxTempAbsolute = pump.maxBasal pumpDescription.basalMaximumRate = pump.maxBasal + pumpDescription.maxBolusSize = pump.maxBolus } override fun loadTDDs(): PumpEnactResult { From 069826ce6b512c863bcd7d41b3c3aabd88a379ed Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:10:33 +0300 Subject: [PATCH 10/29] apex: service: fix race condition in getValue --- .../kotlin/app/aaps/pump/apex/ApexService.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 7cbbcc915010..5d3307e9be92 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -997,10 +997,10 @@ class ApexService: DaggerService(), ApexBluetoothCallback { aapsLogger.debug(LTag.PUMPCOMM, "from PUMP: ${command.id!!.name}, ${type?.name}") if (type == null) return@Thread + if (type == PumpObject.CommandResponse) return@Thread onCommandResponse(CommandResponse(command)) notifyAboutResponse(command, type) when (type) { - PumpObject.CommandResponse -> onCommandResponse(CommandResponse(command)) PumpObject.StatusV1 -> onStatusV1(StatusV1(command)) PumpObject.Heartbeat -> onHeartbeat() PumpObject.BolusEntry -> onBolusEntry(BolusEntry(command)) @@ -1010,8 +1010,14 @@ class ApexService: DaggerService(), ApexBluetoothCallback { }.start() private fun notifyAboutResponse(command: PumpCommand, type: PumpObject) { - if (!getValueResult.waiting) return - if (type != getValueResult.targetObject) return + if (!getValueResult.waiting) { + aapsLogger.debug(LTag.PUMPCOMM, "Got pump command but not waiting for it") + return + } + if (type != getValueResult.targetObject) { + aapsLogger.debug(LTag.PUMPCOMM, "Got incorrect object type (${type.name} vs ${getValueResult.targetObject?.name})") + return + } getValueResult.add( when (type) { @@ -1028,6 +1034,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { ) if (getValueResult.isSingleObject) { + aapsLogger.debug(LTag.PUMPCOMM, "Got single value - everything is ready") getValueResult.waiting = false synchronized(getValueResult) { getValueResult.notifyAll() @@ -1039,10 +1046,11 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } } + @Synchronized private fun runGetThread() { if (isGetThreadRunning) return - aapsLogger.debug(LTag.PUMPCOMM, "Running GET thread") isGetThreadRunning = true + aapsLogger.debug(LTag.PUMPCOMM, "Running GET thread") Thread { while (isGetThreadRunning) { val now = System.currentTimeMillis() @@ -1065,6 +1073,8 @@ class ApexService: DaggerService(), ApexBluetoothCallback { getValueResult.notifyAll() } }.start() + // Let thread start + SystemClock.sleep(10) } //////// Binder From 07442af4d9ac4cf2f40d6259aaf022dc5a6f593c Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:49:59 +0300 Subject: [PATCH 11/29] apex: rework isThisProfileSet implementation --- .../kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index 45550da842c6..7422c927449d 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -55,6 +55,7 @@ import org.json.JSONException import org.json.JSONObject import javax.inject.Inject +import kotlin.math.abs /** * @author Roman Rikhter (teledurak@gmail.com) @@ -290,10 +291,14 @@ class ApexPumpPlugin @Inject constructor( override fun isThisProfileSet(profile: Profile): Boolean { if (!isInitialized()) return false - val profileBasal = profile.toApexReadableProfile() - val pumpBasals = service!!.getBasalProfiles("ApexPumpPlugin-isThisProfileSet") ?: return false - val pumpBasal = pumpBasals[ApexService.USED_BASAL_PATTERN_INDEX] - return pumpBasal == profileBasal + val pumpBasalProfiles = service!!.getBasalProfiles("ApexPumpPlugin-isThisProfileSet") ?: return false + val pumpBasalProfile = pumpBasalProfiles[ApexService.USED_BASAL_PATTERN_INDEX] + for (i in 0..<48) { + val profileBasal = profile.getBasalTimeFromMidnight(i * 30 * 60) + val pumpBasal = pumpBasalProfile!![i] + if (abs(pumpBasal - profileBasal) > 0.01) return false + } + return true } override fun lastDataTime(): Long { From a68369a7bd51c32c19ba341d98d7ccec29bd0581 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Sat, 22 Feb 2025 10:50:15 +0300 Subject: [PATCH 12/29] apex: fix threading issues --- .../kotlin/app/aaps/pump/apex/ApexService.kt | 40 ++++++++++--------- .../pump/apex/connectivity/ApexBluetooth.kt | 6 +-- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 5d3307e9be92..834f20f02556 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -892,13 +892,13 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } - override fun onConnect() = Thread { + override fun onConnect() { aapsLogger.debug(LTag.PUMPCOMM, "onConnect") val version = getValue(GetValue.Value.Version)?.firstOrNull() if (version !is Version) { aapsLogger.error(LTag.PUMPCOMM, "Failed to get version - disconnecting.") - return@Thread disconnect() + return disconnect() } pump.firmwareVersion = version @@ -910,14 +910,14 @@ class ApexService: DaggerService(), ApexBluetoothCallback { rh.gs(R.string.notification_pump_unsupported), Notification.URGENT, ) - return@Thread disconnect() + return disconnect() } onVersion(version) aapsLogger.debug(LTag.PUMPCOMM, "Protocol v${version.protocolMajor}.${version.protocolMinor}") - if (!syncDateTime("BLE-onConnect")) return@Thread - if (!notifyAboutConnection("BLE-onConnect")) return@Thread + if (!syncDateTime("BLE-onConnect")) return + if (!notifyAboutConnection("BLE-onConnect")) return if (apexDeviceInfo.serialNumber != preferences.get(ApexStringKey.LastConnectedSerialNumber)) { onInitialConnection() @@ -925,14 +925,14 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) - if (!getStatus("BLE-onConnect")) return@Thread - if (!getBoluses("BLE-onConnect")) return@Thread + if (!getStatus("BLE-onConnect")) return + if (!getBoluses("BLE-onConnect")) return unreachableTimerTask?.cancel() unreachableTimerTask = null rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) pump.gettingReady = false - }.start() + } private var isDisconnectLoopRunning = false private fun spawnLoop() { @@ -944,7 +944,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { aapsLogger.debug(LTag.PUMPCOMM, "Starting connection loop") startConnection() } - SystemClock.sleep(250) + SystemClock.sleep(100) } if (manualDisconnect) { @@ -957,9 +957,8 @@ class ApexService: DaggerService(), ApexBluetoothCallback { }.start() } - override fun onDisconnect() = Thread { + override fun onDisconnect() { aapsLogger.debug(LTag.PUMPCOMM, "onDisconnect") - getValueResult.clear() isGetThreadRunning = false synchronized(getValueResult) { @@ -985,21 +984,25 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } if (!manualDisconnect) spawnLoop() - }.start() + } private var isGetThreadRunning = false - override fun onPumpCommand(command: PumpCommand) = Thread { + override fun onPumpCommand(command: PumpCommand) { if (command.id == null) { aapsLogger.error(LTag.PUMPCOMM, "Invalid command with crc ${command.checksum}") - return@Thread + return } val type = PumpObject.findObject(command.id!!, command.objectData, aapsLogger) aapsLogger.debug(LTag.PUMPCOMM, "from PUMP: ${command.id!!.name}, ${type?.name}") - if (type == null) return@Thread - if (type == PumpObject.CommandResponse) return@Thread onCommandResponse(CommandResponse(command)) + if (type == null) return + if (type == PumpObject.CommandResponse) return onCommandResponse(CommandResponse(command)) notifyAboutResponse(command, type) + Thread { processObject(command, type) }.start() + } + + private fun processObject(command: PumpCommand, type: PumpObject) { when (type) { PumpObject.StatusV1 -> onStatusV1(StatusV1(command)) PumpObject.Heartbeat -> onHeartbeat() @@ -1007,8 +1010,9 @@ class ApexService: DaggerService(), ApexBluetoothCallback { PumpObject.TDDEntry -> onTDDEntry(TDDEntry(command)) else -> {} } - }.start() + } + @Synchronized private fun notifyAboutResponse(command: PumpCommand, type: PumpObject) { if (!getValueResult.waiting) { aapsLogger.debug(LTag.PUMPCOMM, "Got pump command but not waiting for it") @@ -1059,7 +1063,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } else { aapsLogger.debug(LTag.PUMPCOMM, "Response is not ready yet") } - SystemClock.sleep(250) + SystemClock.sleep(100) } if (!isGetThreadRunning) { aapsLogger.debug(LTag.PUMPCOMM, "GET thread killed") diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt index 5a4815855f72..00e7ee2f3f63 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt @@ -49,7 +49,7 @@ class ApexBluetooth @Inject constructor( private val WRITE_UUID = UUID.fromString("0000FFE9-0000-1000-8000-00805F9B34FB") private val CCC_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB") - private const val WRITE_DELAY_MS = 250 + private const val WRITE_DELAY_MS = 100 } private val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter @@ -225,13 +225,13 @@ class ApexBluetooth @Inject constructor( @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { super.onCharacteristicRead(gatt, characteristic, status) - Thread { onPumpData(characteristic, characteristic.value) }.start() + onPumpData(characteristic, characteristic.value) } @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) - Thread { onPumpData(characteristic, characteristic.value) }.start() + onPumpData(characteristic, characteristic.value) } }, BluetoothDevice.TRANSPORT_LE) From 3d732762b8edb3f937ea0a8f1a873c9133b9d3bc Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:10:52 +0300 Subject: [PATCH 13/29] apex: rework bolus state sync --- .../kotlin/app/aaps/pump/apex/ApexPump.kt | 1 + .../kotlin/app/aaps/pump/apex/ApexService.kt | 42 ++++++++++--------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt index 779ca4215088..57149c4f0230 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt @@ -149,6 +149,7 @@ class ApexPump @Inject constructor() { var cancelled: Boolean = false, var detailedBolusInfo: DetailedBolusInfo, var treatment: EventOverviewBolusProgress.Treatment, + var failed: Boolean = false, ) enum class Update { diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 834f20f02556..140b4c92b3ef 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -139,9 +139,6 @@ class ApexService: DaggerService(), ApexBluetoothCallback { private var getValueLastTaskTimestamp: Long = 0 private var unreachableTimerTask: TimerTask? = null - private var waitingForCurrentBolusInHistory = false - - private var _bolusCompletable: CompletableDeferred? = null private var statusGetValue: GetValue.Value = GetValue.Value.StatusV1 private var lastBolusDateTime = DateTime(0) @@ -322,7 +319,6 @@ class ApexService: DaggerService(), ApexBluetoothCallback { ) ) - _bolusCompletable = CompletableDeferred() getStatus("ApexService-bolus") return true } @@ -573,9 +569,6 @@ class ApexService: DaggerService(), ApexBluetoothCallback { val connectionStatus: ApexBluetooth.Status get() = apexBluetooth.status - val bolusCompletable: CompletableDeferred? - get() = _bolusCompletable - //////// Pump commands handlers private fun onBolusProgress(dose: Double) { @@ -601,7 +594,6 @@ class ApexService: DaggerService(), ApexBluetoothCallback { aapsLogger.debug(LTag.PUMPCOMM, "bolus completed") if (pump.inProgressBolus == null) return pump.inProgressBolus!!.currentDose = dose - waitingForCurrentBolusInHistory = true rxBus.send(EventOverviewBolusProgress.apply { percent = 100 @@ -609,7 +601,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { }) // Request new bolus history to fixup bolus ID. - getBoluses("ApexService-onBolusCompleted") + Thread { getBoluses("ApexService-onBolusCompleted") }.start() } private fun onBolusFailed(cancelled: Boolean = false) { @@ -624,14 +616,16 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } if (pump.inProgressBolus!!.currentDose >= 0.025) { - waitingForCurrentBolusInHistory = true // Request new bolus history to fixup bolus ID and delivered amount. - getBoluses("ApexService-onBolusCompleted") + Thread { getBoluses("ApexService-onBolusCompleted") }.start() } else { aapsLogger.debug(LTag.PUMPCOMM, "bolus entirely failed!") - // TODO: how to handle fully failed boluses? + synchronized(pump.inProgressBolus!!) { + pump.inProgressBolus!!.failed = true + pump.inProgressBolus!!.notifyAll() + } + SystemClock.sleep(10) pump.inProgressBolus = null - _bolusCompletable?.complete(null) } } @@ -811,10 +805,13 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } } - private fun onBolusEntry(entry: BolusEntry) { + private val bolusEntryLock = Mutex() + private fun onBolusEntry(entry: BolusEntry) = synchronized(bolusEntryLock) { // Extended bolus entries do not have duration stored, do not use them. if (entry.extendedDose > 0) return + aapsLogger.debug(LTag.PUMP, "Processing bolus [${entry.standardDose * 0.025}U -> ${entry.standardPerformed * 0.025}U] on ${entry.dateTime}") + if (entry.dateTime > lastBolusDateTime) { lastBolusDateTime = entry.dateTime pump.lastBolus = entry @@ -824,8 +821,13 @@ class ApexService: DaggerService(), ApexBluetoothCallback { // Find the bolus in history and sync it. // Pump may round up boluses, use 0.11 for failsafe. val ipb = pump.inProgressBolus - if (waitingForCurrentBolusInHistory && ipb != null && entry.dateTime.millis >= ipb.temporaryId) { - if (!ipb.cancelled && abs(entry.standardDose - ipb.currentDose) > 0.11) return + if (ipb != null && entry.dateTime.millis - ipb.temporaryId >= -45000) { + aapsLogger.debug(LTag.PUMP, "Syncing current bolus [${entry.standardDose * 0.025}U -> ${entry.standardPerformed * 0.025}U]") + val delta = abs(entry.standardDose * 0.025 - ipb.currentDose) + if (!ipb.cancelled && delta > 0.11) { + aapsLogger.debug(LTag.PUMP, "Not this bolus: $delta > 0.11") + return + } val syncResult = pumpSync.syncBolusWithTempId( timestamp = entry.dateTime.millis, @@ -837,14 +839,16 @@ class ApexService: DaggerService(), ApexBluetoothCallback { type = ipb.detailedBolusInfo.bolusType, ) aapsLogger.debug(LTag.PUMP, "Final bolus [${entry.standardDose * 0.025}U -> ${entry.standardPerformed * 0.025}U] sync succeeded? $syncResult") + synchronized(pump.inProgressBolus!!) { + pump.inProgressBolus!!.notifyAll() + } + SystemClock.sleep(10) pump.inProgressBolus = null - waitingForCurrentBolusInHistory = false - _bolusCompletable?.complete(ipb) - _bolusCompletable = null getStatus("ApexService-updateAfterBolus") return } + if (ipb != null && entry.index < 2) return // Otherwise, just sync the bolus with the DB pumpSync.syncBolusWithPumpId( From 22f66d19d617f0300bf2cd9286287ebc837b267a Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:11:15 +0300 Subject: [PATCH 14/29] apex: increase timeouts --- pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 140b4c92b3ef..e561ba1f0416 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -179,7 +179,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { apexBluetooth.send(GetValue(apexDeviceInfo, value)) try { aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Waiting for response") - getValueResult.waitMillis(if (getValueResult.isSingleObject) 1000 else 5000) + getValueResult.waitMillis(if (getValueResult.isSingleObject) 5000 else 15000) } catch (e: InterruptedException) { aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") isGetThreadRunning = false @@ -200,7 +200,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { apexBluetooth.send(command) try { aapsLogger.debug(LTag.PUMPCOMM, "$command | Waiting for response") - commandResponse.waitMillis(1000) + commandResponse.waitMillis(5000) } catch (e: InterruptedException) { aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") commandResponse.waiting = false From b2cd66a61fbbea7b4711505545d605b9d6705fce Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:12:40 +0300 Subject: [PATCH 15/29] apex: increase battery change percentage trigger Pump may report battery percentage like 75 - 50 - 75%, what creates incorrect pump battery change entry --- pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index e561ba1f0416..dade70283165 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -718,7 +718,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { val cur = update.current update.previous?.let { old -> // Percentage became higher - battery was changed. - if (cur.batteryLevel.percentage - 2 > old.batteryLevel.percentage && preferences.get(ApexBooleanKey.LogBatteryChange)) { + if (cur.batteryLevel.percentage - 26 > old.batteryLevel.percentage && preferences.get(ApexBooleanKey.LogBatteryChange)) { pumpSync.insertTherapyEventIfNewWithTimestamp( timestamp = System.currentTimeMillis(), pumpType = PumpType.APEX_TRUCARE_III, From f637ce84ffdcea85df13721dc22aafc02c9d35a9 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:13:32 +0300 Subject: [PATCH 16/29] apex: status/settings V1 scheme update --- .../commands/device/UpdateSettingsV1.kt | 16 +++++-- .../connectivity/commands/pump/StatusV1.kt | 42 +++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt index de721f3c63a9..f8696dd290e8 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/device/UpdateSettingsV1.kt @@ -42,6 +42,9 @@ class UpdateSettingsV1( val maxTDD: Int, val maxBasalRate: Int, val maxSingleBolus: Int, + val enableGlucoseReminder: Boolean = false, + val enableAutoSuspend: Boolean = false, + val lockPump: Boolean = false, ) : BaseValueCommand(info) { override val valueId = 0x32 override val isWrite = true @@ -51,13 +54,17 @@ class UpdateSettingsV1( override val additionalData: ByteArray get() { var functionFlags = 0 - var bolusFlags = 1 shl 1 + var bolusFlags = 0 if (bolusSpeed == BolusDeliverySpeed.Low) functionFlags = functionFlags or FunctionFlags.LowBolusSpeed.raw if (language == Language.English) functionFlags = functionFlags or FunctionFlags.EnglishLanguage.raw if (limitTDD) functionFlags = functionFlags or FunctionFlags.TDDLimit.raw if (lockKeys) functionFlags = functionFlags or FunctionFlags.KeyboardLock.raw + if (enableAutoSuspend) functionFlags = functionFlags or FunctionFlags.AutoSuspend.raw + if (lockPump) functionFlags = functionFlags or FunctionFlags.LockPump.raw + if (enableAdvancedBolus) bolusFlags = bolusFlags or BolusFlags.AdvancedBolus.raw + if (enableGlucoseReminder) bolusFlags = bolusFlags or BolusFlags.BGReminder.raw return byteArrayOf( functionFlags.toByte(), @@ -77,12 +84,15 @@ class UpdateSettingsV1( private enum class FunctionFlags(val raw: Int) { LowBolusSpeed(1 shl 0), KeyboardLock(1 shl 1), - TDDLimit(1 shl 2 and 1 shl 4), - EnglishLanguage(1 shl 3 and 1 shl 5), + AutoSuspend(1 shl 2), + LockPump(1 shl 4), + TDDLimit(1 shl 5), + EnglishLanguage( 1 shl 6), } private enum class BolusFlags(val raw: Int) { AdvancedBolus(1 shl 0), + BGReminder(1 shl 1), } override fun toString(): String = "UpdateSettingsV1(maxTDD = $maxTDD, maxBolus = $maxSingleBolus, maxBasal = $maxBasalRate, bolusSpeed = ${bolusSpeed.name}, ...)" diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt index 039603d7e257..0a089a9474d3 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV1.kt @@ -23,12 +23,16 @@ class StatusV1(command: PumpCommand): PumpObjectModel() { private val bolusFlags = command.objectData[6].toUByte().toInt() private enum class BolusFlags(val raw: Int) { - AdvancedBolusEnabled(1 shl 1) + AdvancedBolusEnabled(1 shl 1), + BGReminderEnabled(1 shl 2), } /** Are dual and extended bolus types enabled? */ val advancedBolusEnabled = (bolusFlags and BolusFlags.AdvancedBolusEnabled.raw) == 1 + /** Is BG reminder alarm enabled? */ + val bgReminderEnabled = (bolusFlags and BolusFlags.BGReminderEnabled.raw) == 1 + /** Keys lock enabled? */ val keyboardLockEnabled = command.objectData[7].toBoolean() @@ -44,7 +48,11 @@ class StatusV1(command: PumpCommand): PumpObjectModel() { /** Low reservoir alarm (triggered by time left) threshold in 30 minute steps */ val lowReservoirTimeLeftThreshold = command.objectData[11].toUByte().toInt() - // Byte 10, 11 unknown + /** Is using preset basal pattern? */ + val isDefaultBasal = command.objectData[12].toBoolean() + + /** Is pump locked? */ + val isLocked = command.objectData[12].toBoolean() /** Current basal pattern index */ val currentBasalPattern = command.objectData[14].toUByte().toInt() @@ -67,7 +75,29 @@ class StatusV1(command: PumpCommand): PumpObjectModel() { /** Maximum bolus in 0.025U steps */ val maxBolus = getUnsignedShort(command.objectData, 28) - // Byte 29-45 unknown + /** Bolus preset: Breakfast A 5:00-7:00 */ + val presetBreakfastA = getUnsignedShort(command.objectData, 30) + + /** Bolus preset: Breakfast B 7:00-10:00 */ + val presetBreakfastB = getUnsignedShort(command.objectData, 32) + + /** Bolus preset: Dinner A 10:00-12:00 */ + val presetDinnerA = getUnsignedShort(command.objectData, 34) + + /** Bolus preset: Dinner B 12:00-15:00 */ + val presetDinnerB = getUnsignedShort(command.objectData, 36) + + /** Bolus preset: Supper A 15:00-18:00 */ + val presetSupperA = getUnsignedShort(command.objectData, 38) + + /** Bolus preset: Supper B 18:00-22:00 */ + val presetSupperB = getUnsignedShort(command.objectData, 40) + + /** Bolus preset: Night A 22:00-0:00 */ + val presetNightA = getUnsignedShort(command.objectData, 42) + + /** Bolus preset: Night B 0:00-5:00 */ + val presetNightB = getUnsignedShort(command.objectData, 44) /** System date and time */ val dateTime = DateTime( @@ -132,6 +162,9 @@ class StatusV1(command: PumpCommand): PumpObjectModel() { maxTDD: Int? = null, maxBasalRate: Int? = null, maxSingleBolus: Int? = null, + enableGlucoseReminder: Boolean? = null, + enableAutoSuspend: Boolean? = null, + lockPump: Boolean? = null, ): UpdateSettingsV1 { return UpdateSettingsV1( info, @@ -149,6 +182,9 @@ class StatusV1(command: PumpCommand): PumpObjectModel() { maxTDD ?: this.maxTDD, maxBasalRate ?: this.maxBasal, maxSingleBolus ?: this.maxBolus, + enableGlucoseReminder ?: this.bgReminderEnabled, + enableAutoSuspend ?: this.autoSuspendEnabled, + lockPump ?: this.isLocked, ) } } \ No newline at end of file From 29767b3efc6c79881e09ea4072e09d3a04748713 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:13:48 +0300 Subject: [PATCH 17/29] apex: new strings --- pump/apex/src/main/res/values/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pump/apex/src/main/res/values/strings.xml b/pump/apex/src/main/res/values/strings.xml index 284b073833da..b223f7a98431 100644 --- a/pump/apex/src/main/res/values/strings.xml +++ b/pump/apex/src/main/res/values/strings.xml @@ -56,13 +56,15 @@ Pump is suspended Pump has unsupported firmware + Pump alarm: %1$s Occlusion Low battery Low reservoir level Battery is dead Reservoir is empty - Hardware error ($1%s) - Unknown error $1%s, report to developers + Hardware error (%1$s) + Check blood glucose + Unknown error %1$s, report to developers Unknown error Delivering %1$.3fU From 6166a316c9b48d96017d81e48738969265ac7886 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:14:17 +0300 Subject: [PATCH 18/29] apex: bluetooth: change delays and rework logging --- .../aaps/pump/apex/connectivity/ApexBluetooth.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt index 00e7ee2f3f63..7d54aca8cfd3 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/ApexBluetooth.kt @@ -49,7 +49,7 @@ class ApexBluetooth @Inject constructor( private val WRITE_UUID = UUID.fromString("0000FFE9-0000-1000-8000-00805F9B34FB") private val CCC_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB") - private const val WRITE_DELAY_MS = 100 + private const val WRITE_DELAY_MS = 250 } private val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter @@ -177,7 +177,7 @@ class ApexBluetooth @Inject constructor( } BluetoothGatt.STATE_CONNECTED -> { bluetoothGatt?.discoverServices() - aapsLogger.debug(LTag.PUMPBTCOMM, "First stage of connection is done") + aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting | Discovering services") } } } @@ -185,6 +185,7 @@ class ApexBluetooth @Inject constructor( override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { super.onMtuChanged(gatt, mtu, status) this@ApexBluetooth.mtu = mtu + aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting | Got MTU $mtu") } @Suppress("DEPRECATION") @@ -195,6 +196,8 @@ class ApexBluetooth @Inject constructor( gatt.requestMtu(512) SystemClock.sleep(150) + aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting | Requesting notification") + writeCharacteristic = gatt.getService(WRITE_SERVICE.uuid).getCharacteristic(WRITE_UUID) readCharacteristic = gatt.getService(READ_SERVICE.uuid).getCharacteristic(READ_UUID) gatt.setCharacteristicNotification(readCharacteristic, true) @@ -213,12 +216,11 @@ class ApexBluetooth @Inject constructor( super.onDescriptorWrite(gatt, descriptor, status) Thread { aapsLogger.debug(LTag.PUMPBTCOMM, "Connected | Notification status: $status") - SystemClock.sleep(100) gatt?.setCharacteristicNotification(readCharacteristic, true) - SystemClock.sleep(100) + SystemClock.sleep(10) _status = Status.CONNECTED callback?.onConnect() - aapsLogger.debug(LTag.PUMPBTCOMM, "Connected successfully") + aapsLogger.debug(LTag.PUMPBTCOMM, "Connected") }.start() } @@ -236,6 +238,7 @@ class ApexBluetooth @Inject constructor( }, BluetoothDevice.TRANSPORT_LE) if (bluetoothGatt == null) { + aapsLogger.error(LTag.PUMPBTCOMM, "Connecting | Failed to set up GATT") _status = Status.DISCONNECTED } } @@ -254,7 +257,6 @@ class ApexBluetooth @Inject constructor( while (lastCommand != null && lastCommand!!.isCompleteCommand()) { if (!lastCommand!!.verify()) { - // TODO: find a better way to do the same thing aapsLogger.error(LTag.PUMPBTCOMM, "[${lastCommand!!.id?.name}] Command checksum is invalid! Expected ${lastCommand!!.checksum.toHex()}") return } @@ -269,7 +271,7 @@ class ApexBluetooth @Inject constructor( @SuppressLint("MissingPermission") @Synchronized private fun reconnect() { - aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to pump...") + aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting | Setting up GATT") bluetoothDevice = bluetoothAdapter!!.getRemoteDevice(preferences.get(ApexStringKey.BluetoothAddress)) setupGatt() } From 5f9039ceb30240ad232866427555d1b81426fa23 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:15:10 +0300 Subject: [PATCH 19/29] apex: round basal using DoseStepSize precision --- .../src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index 7422c927449d..48d51387667d 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -9,6 +9,7 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceManager import androidx.preference.PreferenceScreen import app.aaps.core.data.plugin.PluginType +import app.aaps.core.data.pump.defs.DoseStepSize import app.aaps.core.data.pump.defs.ManufacturerType import app.aaps.core.data.pump.defs.PumpDescription import app.aaps.core.data.pump.defs.PumpType @@ -296,7 +297,11 @@ class ApexPumpPlugin @Inject constructor( for (i in 0..<48) { val profileBasal = profile.getBasalTimeFromMidnight(i * 30 * 60) val pumpBasal = pumpBasalProfile!![i] - if (abs(pumpBasal - profileBasal) > 0.01) return false + val precision = DoseStepSize.Apex.getStepSizeForAmount(profileBasal) / 2 + if (abs(pumpBasal - profileBasal) > precision) { + aapsLogger.info(LTag.PUMP, "Profiles are not same: req $profileBasal != pump $pumpBasal, block $i, time ${i * 30 * 60}") + return false + } } return true } From a0d56c45afc95ff24587e34e14c187f4e0b24d8a Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:15:34 +0300 Subject: [PATCH 20/29] apex: plugin: implement new bolus sync as in service --- .../app/aaps/pump/apex/ApexPumpPlugin.kt | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index 48d51387667d..f715367b110a 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -36,13 +36,12 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.Preferences import app.aaps.core.objects.constraints.ConstraintObject +import app.aaps.core.utils.wait import app.aaps.core.validators.preferences.AdaptiveDoublePreference import app.aaps.core.validators.preferences.AdaptiveListPreference import app.aaps.core.validators.preferences.AdaptiveStringPreference import app.aaps.pump.apex.connectivity.ApexBluetooth import app.aaps.pump.apex.connectivity.commands.pump.AlarmLength -import app.aaps.pump.apex.connectivity.commands.pump.BolusEntry -import app.aaps.pump.apex.connectivity.commands.pump.Version import app.aaps.pump.apex.ui.ApexFragment import app.aaps.pump.apex.utils.keys.ApexBooleanKey import app.aaps.pump.apex.utils.keys.ApexDoubleKey @@ -50,8 +49,6 @@ import app.aaps.pump.apex.utils.keys.ApexStringKey import app.aaps.pump.apex.utils.toApexReadableProfile import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.runBlocking import org.json.JSONException import org.json.JSONObject @@ -345,14 +342,19 @@ class ApexPumpPlugin @Inject constructor( } } - return runBlocking { - val bolus = service!!.bolusCompletable?.await() - pumpEnactResult.apply { - success = bolus != null - if (bolus != null) { - enacted = bolus.currentDose >= 0.025 - bolusDelivered = bolus.currentDose - } + pump.inProgressBolus?.let { + synchronized(it) { + it.wait() + } + } + + val bolus = pump.inProgressBolus + val successful = bolus != null && !bolus.failed + return pumpEnactResult.apply { + success = successful + if (successful) { + enacted = bolus!!.currentDose >= 0.025 + bolusDelivered = bolus.currentDose } } } From 83baaafcec9d0933ced6c2952a252d7c2a09c074 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 09:09:37 +0300 Subject: [PATCH 21/29] apex: improve alarms handling --- .../kotlin/app/aaps/pump/apex/ApexPump.kt | 1 - .../kotlin/app/aaps/pump/apex/ApexService.kt | 26 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt index 57149c4f0230..d1dbf3d56cb3 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt @@ -65,7 +65,6 @@ class ApexPump @Inject constructor() { var lastV1: StatusV1? = null var inProgressBolus: InProgressBolus? = null - var isAlarmPresent: Boolean = false var lastBolus: BolusEntry? = null var firmwareVersion: Version? = null var serialNumber: String = "" diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index dade70283165..9fcc71066c71 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -648,20 +648,16 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } private fun onAlarmsChanged(update: ApexPump.StatusUpdate) { + val prev = update.previous?.alarms // Alarm was dismissed - if (pump.isAlarmPresent && update.current.alarms.isEmpty()) { - pump.isAlarmPresent = false + if (!prev.isNullOrEmpty() && update.current.alarms.isEmpty()) { rxBus.send(EventDismissNotification(Notification.PUMP_ERROR)) rxBus.send(EventDismissNotification(Notification.PUMP_WARNING)) } // New alarms - if (!pump.isAlarmPresent && update.current.alarms.isNotEmpty()) { - if (pump.isBolusing) { - // Pump sends early heartbeat while bolusing if there's an error while bolusing. - aapsLogger.error(LTag.PUMP, "Bolus has failed!") - onBolusFailed() - } + if (prev.isNullOrEmpty() && update.current.alarms.isNotEmpty()) { + var anyUrgent = false for (alarm in update.current.alarms) { val name = when (alarm) { @@ -674,24 +670,32 @@ class ApexService: DaggerService(), ApexBluetoothCallback { Alarm.TimeAnomalyError, Alarm.MotorAbnormal, Alarm.MotorPowerAbnormal, Alarm.MotorError -> rh.gs(R.string.alarm_hardware_fault, alarm.name) Alarm.Unknown -> rh.gs(R.string.alarm_unknown_error) + Alarm.CheckGlucose -> rh.gs(R.string.alarm_check_bg) else -> rh.gs(R.string.alarm_unknown_error_name, alarm.name) } val isUrgent = when(alarm) { - Alarm.LowBattery, Alarm.LowReservoir -> false + Alarm.LowBattery, Alarm.LowReservoir, Alarm.CheckGlucose -> false else -> true } + if (isUrgent) anyUrgent = true uiInteraction.addNotification( if (isUrgent) Notification.PUMP_ERROR else Notification.PUMP_WARNING, - name, + rh.gs(R.string.alarm_label, name), if (isUrgent) Notification.URGENT else Notification.NORMAL, ) pumpSync.insertAnnouncement( - error = name, + error = rh.gs(R.string.alarm_label, name), pumpType = PumpType.APEX_TRUCARE_III, pumpSerial = apexDeviceInfo.serialNumber, ) } + + if (anyUrgent && pump.isBolusing) { + // Pump sends early heartbeat while bolusing if there's an error while bolusing. + aapsLogger.error(LTag.PUMP, "Bolus has failed!") + Thread { onBolusFailed() }.start() + } } } From dfba9e4a090219911e102b82380c53d56f82d4f1 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 25 Feb 2025 21:36:05 +0300 Subject: [PATCH 22/29] apex: make critical sections synchronized --- .../main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt | 12 ++++++++++++ .../main/kotlin/app/aaps/pump/apex/ApexService.kt | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt index f715367b110a..5f2014548e9d 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPumpPlugin.kt @@ -230,6 +230,7 @@ class ApexPumpPlugin @Inject constructor( pumpDescription.maxBolusSize = pump.maxBolus } + @Synchronized override fun loadTDDs(): PumpEnactResult { val ret = instantiator.providePumpEnactResult() if (!isInitialized()) { @@ -247,12 +248,14 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun getPumpStatus(reason: String) { if (!isInitialized()) return aapsLogger.debug(LTag.PUMP, "Requested pump status cause of $reason") if (!service!!.getStatus("ApexPumpPlugin-getPumpStatus")) return } + @Synchronized override fun setNewBasalProfile(profile: Profile): PumpEnactResult { val ret = instantiator.providePumpEnactResult() if (!isInitialized()) { @@ -287,6 +290,7 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun isThisProfileSet(profile: Profile): Boolean { if (!isInitialized()) return false val pumpBasalProfiles = service!!.getBasalProfiles("ApexPumpPlugin-isThisProfileSet") ?: return false @@ -308,6 +312,7 @@ class ApexPumpPlugin @Inject constructor( return service!!.lastConnected } + @Synchronized override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { // Insulin value must be greater than 0 require(detailedBolusInfo.carbs == 0.0) { detailedBolusInfo.toString() } @@ -359,11 +364,13 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun stopBolusDelivering() { if (!isInitialized()) return service!!.cancelBolus("ApexPumpPlugin-stopBolusDelivering") } + @Synchronized override fun setTempBasalAbsolute(absoluteRate: Double, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { val pumpEnactResult = instantiator.providePumpEnactResult() val rate = constraintsChecker @@ -414,6 +421,7 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun setTempBasalPercent(percent: Int, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { return instantiator.providePumpEnactResult().apply { success = false @@ -422,6 +430,7 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult { val pumpEnactResult = instantiator.providePumpEnactResult() if (!isInitialized()) { @@ -455,6 +464,7 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun setExtendedBolus(insulin: Double, durationInMinutes: Int): PumpEnactResult { // Not yet supported return instantiator.providePumpEnactResult().apply { @@ -464,6 +474,7 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun cancelExtendedBolus(): PumpEnactResult { // Not yet supported return instantiator.providePumpEnactResult().apply { @@ -473,6 +484,7 @@ class ApexPumpPlugin @Inject constructor( } } + @Synchronized override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) { if (!isInitialized()) return service!!.syncDateTime("ApexService-timezoneOrDSTChanged") diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 9fcc71066c71..2e381b8fa088 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -809,8 +809,8 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } } - private val bolusEntryLock = Mutex() - private fun onBolusEntry(entry: BolusEntry) = synchronized(bolusEntryLock) { + @Synchronized + private fun onBolusEntry(entry: BolusEntry) { // Extended bolus entries do not have duration stored, do not use them. if (entry.extendedDose > 0) return From a81589528f9ad50c01fa921ce3c0d091514a6f99 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 18 Mar 2025 17:04:05 +0300 Subject: [PATCH 23/29] apex: initial 4.11 protocol support --- .../kotlin/app/aaps/pump/apex/ApexPump.kt | 40 ++++++++++-- .../kotlin/app/aaps/pump/apex/ApexService.kt | 63 ++++++++++--------- .../connectivity/commands/pump/PumpObjects.kt | 1 + .../connectivity/commands/pump/StatusV2.kt | 6 ++ .../connectivity/commands/pump/Version.kt | 2 + 5 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV2.kt diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt index d1dbf3d56cb3..a5651cf585cb 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt @@ -6,6 +6,7 @@ import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress import app.aaps.pump.apex.connectivity.commands.pump.Alarm import app.aaps.pump.apex.connectivity.commands.pump.BolusEntry import app.aaps.pump.apex.connectivity.commands.pump.StatusV1 +import app.aaps.pump.apex.connectivity.commands.pump.StatusV2 import app.aaps.pump.apex.connectivity.commands.pump.Version import org.joda.time.DateTime import javax.inject.Inject @@ -21,7 +22,7 @@ class ApexPump @Inject constructor() { get() = _status val batteryLevel: BatteryLevel - get() = status?.batteryLevel ?: BatteryLevel(0, 0, true) + get() = status?.batteryLevel ?: BatteryLevel(0, 0.0, true) val isAdvancedBolusEnabled: Boolean get() = status?.isAdvancedBolusEnabled ?: false @@ -59,10 +60,8 @@ class ApexPump @Inject constructor() { val settingsAreUnadvised: Boolean get() = isAdvancedBolusEnabled || currentBasalPattern != ApexService.USED_BASAL_PATTERN_INDEX - val isV1: Boolean - get() = lastV1 != null - var lastV1: StatusV1? = null + var lastV2: StatusV2? = null var inProgressBolus: InProgressBolus? = null var lastBolus: BolusEntry? = null @@ -76,7 +75,7 @@ class ApexPump @Inject constructor() { fun updateFromV1(obj: StatusV1): StatusUpdate { val updates = arrayListOf() val new = PumpStatus( - batteryLevel = BatteryLevel(obj.batteryLevel!!.approximatePercentage, null, true), + batteryLevel = BatteryLevel(obj.batteryLevel!!.approximatePercentage, batteryLevel.voltage, true), isAdvancedBolusEnabled = obj.advancedBolusEnabled, currentBasalPattern = obj.currentBasalPattern, maxBasal = obj.maxBasal * 0.025, @@ -121,9 +120,38 @@ class ApexPump @Inject constructor() { return ret } + fun updateFromV2(obj: StatusV2): StatusUpdate { + val new = PumpStatus( + batteryLevel = BatteryLevel(batteryLevel.percentage, obj.batteryVoltage, batteryLevel.approximate), + dateTime = dateTime, + reservoirLevel = reservoirLevel, + tbr = tbr, + alarms = alarms, + currentBasalPattern = currentBasalPattern, + maxBasal = maxBasal, + maxBolus = maxBolus, + basal = basal, + isAdvancedBolusEnabled = isAdvancedBolusEnabled, + ) + val updates = mutableListOf() + updates.apply { when { + batteryLevel.voltage != new.batteryLevel.voltage -> add(Update.BatteryChanged) + } } + + lastV2 = obj + + val ret = StatusUpdate( + changes = updates, + previous = status, + current = new, + ) + _status = new + return ret + } + data class BatteryLevel( val percentage: Int, - val voltage: Int?, // v2+ + val voltage: Double?, // v2+ val approximate: Boolean ) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 2e381b8fa088..6fa744efeee1 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -53,6 +53,7 @@ import app.aaps.pump.apex.connectivity.commands.pump.PumpCommand import app.aaps.pump.apex.connectivity.commands.pump.PumpObject import app.aaps.pump.apex.connectivity.commands.pump.PumpObjectModel import app.aaps.pump.apex.connectivity.commands.pump.StatusV1 +import app.aaps.pump.apex.connectivity.commands.pump.StatusV2 import app.aaps.pump.apex.connectivity.commands.pump.TDDEntry import app.aaps.pump.apex.connectivity.commands.pump.Version import app.aaps.pump.apex.events.EventApexPumpDataChanged @@ -63,7 +64,6 @@ import app.aaps.pump.apex.utils.keys.ApexStringKey import dagger.android.DaggerService import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.sync.Mutex import org.joda.time.DateTime import java.util.Timer @@ -94,7 +94,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { companion object { const val USED_BASAL_PATTERN_INDEX = 7 val FIRST_SUPPORTED_PROTO = ProtocolVersion.PROTO_4_10 - val LAST_SUPPORTED_PROTO = ProtocolVersion.PROTO_4_10 + val LAST_SUPPORTED_PROTO = ProtocolVersion.PROTO_4_11 } private data class InCommandResponse( @@ -139,8 +139,6 @@ class ApexService: DaggerService(), ApexBluetoothCallback { private var getValueLastTaskTimestamp: Long = 0 private var unreachableTimerTask: TimerTask? = null - private var statusGetValue: GetValue.Value = GetValue.Value.StatusV1 - private var lastBolusDateTime = DateTime(0) private var lastConnectedTimestamp = System.currentTimeMillis() @@ -163,12 +161,13 @@ class ApexService: DaggerService(), ApexBluetoothCallback { getValueResult.clear() getValueResult.targetObject = when (value) { GetValue.Value.StatusV1 -> PumpObject.StatusV1 + GetValue.Value.StatusV2 -> PumpObject.StatusV2 GetValue.Value.TDDs -> PumpObject.TDDEntry GetValue.Value.Alarms -> PumpObject.AlarmEntry GetValue.Value.BasalProfiles -> PumpObject.BasalProfile GetValue.Value.Version -> PumpObject.FirmwareEntry GetValue.Value.BolusHistory, GetValue.Value.LatestBoluses -> PumpObject.BolusEntry - GetValue.Value.LatestTemporaryBasals, GetValue.Value.StatusV2 -> return null // TODO 4.11 bring up + GetValue.Value.LatestTemporaryBasals -> return null GetValue.Value.WizardStatus -> return null } getValueResult.isSingleObject = when (value) { @@ -430,16 +429,13 @@ class ApexService: DaggerService(), ApexBluetoothCallback { fun updateSettings(caller: String): Boolean { aapsLogger.debug(LTag.PUMPCOMM, "updateSettings - $caller") val response = executeWithResponse( - if (pump.isV1) - pump.lastV1!!.toUpdateSettingsV1( - apexDeviceInfo, - AlarmLength.valueOf(preferences.get(ApexStringKey.AlarmSoundLength)), - maxSingleBolus = (preferences.get(ApexDoubleKey.MaxBolus) / 0.025).roundToInt(), - maxBasalRate = (preferences.get(ApexDoubleKey.MaxBasal) / 0.025).roundToInt(), - enableAdvancedBolus = false, - ) - else - return false + pump.lastV1!!.toUpdateSettingsV1( + apexDeviceInfo, + AlarmLength.valueOf(preferences.get(ApexStringKey.AlarmSoundLength)), + maxSingleBolus = (preferences.get(ApexDoubleKey.MaxBolus) / 0.025).roundToInt(), + maxBasalRate = (preferences.get(ApexDoubleKey.MaxBasal) / 0.025).roundToInt(), + enableAdvancedBolus = false, + ) ) if (response == null) { aapsLogger.error(LTag.PUMPCOMM, "[updateSettings caller=$caller] Timed out while trying to communicate with the pump") @@ -536,12 +532,20 @@ class ApexService: DaggerService(), ApexBluetoothCallback { fun getStatus(caller: String): Boolean { rxBus.send(EventPumpStatusChanged(rh.gs(R.string.getting_pump_status))) aapsLogger.debug(LTag.PUMPCOMM, "getStatus - $caller") - val response = getValue(statusGetValue) - if (response == null) { - aapsLogger.error(LTag.PUMPCOMM, "[getStatus caller=$caller] Timed out while trying to communicate with the pump") + val responseV1 = getValue(GetValue.Value.StatusV1) + if (responseV1 == null) { + aapsLogger.error(LTag.PUMPCOMM, "[getStatus caller=$caller] V1 | Timed out while trying to communicate with the pump") return false } + if ((pump.firmwareVersion?.protocolMinor ?: 0) >= 11) { + val responseV2 = getValue(GetValue.Value.StatusV2) + if (responseV2 == null) { + aapsLogger.error(LTag.PUMPCOMM, "[getStatus caller=$caller] V2 | Timed out while trying to communicate with the pump") + return false + } + } + return true } @@ -768,8 +772,9 @@ class ApexService: DaggerService(), ApexBluetoothCallback { preferences.put(ApexDoubleKey.MaxBolus, update.current.maxBolus) } - private fun onStatusCommon(update: ApexPump.StatusUpdate) { - aapsLogger.debug(LTag.PUMPCOMM, "Status updates: ${update.changes.joinToString(", ") { it.name }}") + private fun onStatusV1(status: StatusV1) { + val update = pump.updateFromV1(status) + aapsLogger.debug(LTag.PUMPCOMM, "Got V1 | Status updates: ${update.changes.joinToString(", ") { it.name }}") preferences.put(ApexDoubleKey.MaxBasal, update.current.maxBasal) preferences.put(ApexDoubleKey.MaxBolus, update.current.maxBolus) @@ -782,12 +787,15 @@ class ApexService: DaggerService(), ApexBluetoothCallback { onReservoirChanged(update) onTBRChanged(update) onConstraintsChanged(update) + rxBus.send(EventApexPumpDataChanged()) } - private fun onStatusV1(status: StatusV1) { - aapsLogger.debug(LTag.PUMPCOMM, "Got status V1") - val updates = pump.updateFromV1(status) - onStatusCommon(updates) + private fun onStatusV2(status: StatusV2) { + val update = pump.updateFromV2(status) + aapsLogger.debug(LTag.PUMPCOMM, "Got V2 | Status updates: ${update.changes.joinToString(", ") { it.name }}") + + //onBatteryChanged(update) + rxBus.send(EventApexPumpDataChanged()) } private fun onHeartbeat() { @@ -803,10 +811,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } private fun onVersion(version: Version) { - aapsLogger.debug(LTag.PUMPCOMM, "Got version") - if (version.atleastProto(ProtocolVersion.PROTO_4_11)) { - statusGetValue = GetValue.Value.StatusV2 - } + aapsLogger.debug(LTag.PUMPCOMM, "Got version - $version") } @Synchronized @@ -1013,6 +1018,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { private fun processObject(command: PumpCommand, type: PumpObject) { when (type) { PumpObject.StatusV1 -> onStatusV1(StatusV1(command)) + PumpObject.StatusV2 -> onStatusV2(StatusV2(command)) PumpObject.Heartbeat -> onHeartbeat() PumpObject.BolusEntry -> onBolusEntry(BolusEntry(command)) PumpObject.TDDEntry -> onTDDEntry(TDDEntry(command)) @@ -1036,6 +1042,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { PumpObject.Heartbeat -> Heartbeat() PumpObject.CommandResponse -> CommandResponse(command) PumpObject.StatusV1 -> StatusV1(command) + PumpObject.StatusV2 -> StatusV2(command) PumpObject.BasalProfile -> BasalProfile(command) PumpObject.AlarmEntry -> AlarmObject(command) PumpObject.TDDEntry -> TDDEntry(command) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt index 726460e07891..9cb963a727d6 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/PumpObjects.kt @@ -12,6 +12,7 @@ enum class PumpObject( Heartbeat(commandId = CommandId.Heartbeat), CommandResponse(commandId = CommandId.SetValue), StatusV1(valueId = listOf(0x00)), + StatusV2(valueId = listOf(0x0C)), WizardStatus(valueId = listOf(0x07)), BasalProfile(valueId = listOf(0x08)), AlarmEntry(valueId = listOf(0x03)), diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV2.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV2.kt new file mode 100644 index 000000000000..43760503a437 --- /dev/null +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/StatusV2.kt @@ -0,0 +1,6 @@ +package app.aaps.pump.apex.connectivity.commands.pump + +class StatusV2(command: PumpCommand): PumpObjectModel() { + /** Pump battery voltage */ + val batteryVoltage = command.objectData[4].toUByte().toInt() / 100.0 +} diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt index 0dd016f1dfcf..13174a9eafe0 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/connectivity/commands/pump/Version.kt @@ -31,4 +31,6 @@ class Version(command: PumpCommand): PumpObjectModel() { if (max.minor < protocolMinor) return false return true } + + override fun toString(): String = "Version(fw = $firmwareMajor.$firmwareMinor, proto = $protocolMajor.$protocolMinor)" } From 809623b44a170f4fbe00c104acb3421b8a4647a1 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Tue, 18 Mar 2025 17:07:04 +0300 Subject: [PATCH 24/29] apex: fragment: fix voltage visibility logic --- pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt index a5651cf585cb..cfe39eb327ca 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt @@ -214,7 +214,7 @@ class ApexPump @Inject constructor() { else -> rh.gs(R.string.overview_pump_status_normal) } - fun getBatteryLevel(rh: ResourceHelper): String = if (batteryLevel.approximate) + fun getBatteryLevel(rh: ResourceHelper): String = if (batteryLevel.voltage == null) rh.gs(R.string.overview_pump_battery_approximate, batteryLevel.percentage) else rh.gs(R.string.overview_pump_battery_exact, batteryLevel.percentage, batteryLevel.voltage) From ce4126b20216340c6ec5e778a44c6d856d20fbe6 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Thu, 20 Mar 2025 16:23:17 +0300 Subject: [PATCH 25/29] apex: do not handle fake bolus entry --- .../src/main/kotlin/app/aaps/pump/apex/ApexService.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 6fa744efeee1..19bc4bddf087 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -1037,6 +1037,17 @@ class ApexService: DaggerService(), ApexBluetoothCallback { return } + // Pump may send fake bolus entry if there are no boluses in history. + // We shouldn't handle it. + if (command.objectData[2].toUInt().toInt() == 0xFF && command.objectData[3].toUInt().toInt() == 0xFF) { + aapsLogger.debug(LTag.PUMPCOMM, "Got fake bolus entry - skipping") + getValueResult.waiting = false + synchronized(getValueResult) { + getValueResult.notifyAll() + } + return + } + getValueResult.add( when (type) { PumpObject.Heartbeat -> Heartbeat() From 51264bb77896cdbbf7f5b140e8d38307993ad016 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Thu, 20 Mar 2025 17:48:48 +0300 Subject: [PATCH 26/29] apex: more aggressive reconnects --- .../kotlin/app/aaps/pump/apex/ApexService.kt | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 19bc4bddf087..8b6bf5a3285f 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -182,6 +182,14 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } catch (e: InterruptedException) { aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") isGetThreadRunning = false + disconnect(true) + return null + } + + if (getValueResult.response == null) { + aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") + isGetThreadRunning = false + disconnect(true) return null } @@ -203,6 +211,14 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } catch (e: InterruptedException) { aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") commandResponse.waiting = false + disconnect(true) + return null + } + + if (commandResponse.response == null) { + aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") + commandResponse.waiting = false + disconnect(true) return null } @@ -899,9 +915,10 @@ class ApexService: DaggerService(), ApexBluetoothCallback { apexBluetooth.connect() } - fun disconnect() { - manualDisconnect = true + fun disconnect(isReconnect: Boolean = false) { + manualDisconnect = !isReconnect if (apexBluetooth.status != ApexBluetooth.Status.DISCONNECTED) apexBluetooth.disconnect() + if (isReconnect) apexBluetooth.connect() } @@ -1042,6 +1059,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { if (command.objectData[2].toUInt().toInt() == 0xFF && command.objectData[3].toUInt().toInt() == 0xFF) { aapsLogger.debug(LTag.PUMPCOMM, "Got fake bolus entry - skipping") getValueResult.waiting = false + getValueResult.response = arrayListOf() synchronized(getValueResult) { getValueResult.notifyAll() } From 333b911437ed6923bc5329b4f5692dd14f3d2d33 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Thu, 20 Mar 2025 18:52:35 +0300 Subject: [PATCH 27/29] apex: rework reconnect --- .../kotlin/app/aaps/pump/apex/ApexService.kt | 213 ++++++++++++------ 1 file changed, 139 insertions(+), 74 deletions(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt index 8b6bf5a3285f..7cc959fa14db 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexService.kt @@ -143,6 +143,8 @@ class ApexService: DaggerService(), ApexBluetoothCallback { private var lastConnectedTimestamp = System.currentTimeMillis() private var manualDisconnect = false + private var doNotReconnect = false + private var connectionFinished = false val isBusy: Boolean get() = commandLock.isLocked @@ -152,79 +154,125 @@ class ApexService: DaggerService(), ApexBluetoothCallback { lastConnectedTimestamp } else System.currentTimeMillis() - fun getValue(value: GetValue.Value): List? = synchronized(commandLock) { synchronized(getValueResult) { - if (connectionStatus != ApexBluetooth.Status.CONNECTED) { - aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Error - pump is disconnected") - return null - } + private fun intGetValue(value: GetValue.Value): List? = synchronized(getValueResult) { + if (connectionStatus != ApexBluetooth.Status.CONNECTED) { + aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Error - pump is disconnected") + return null + } - getValueResult.clear() - getValueResult.targetObject = when (value) { - GetValue.Value.StatusV1 -> PumpObject.StatusV1 - GetValue.Value.StatusV2 -> PumpObject.StatusV2 - GetValue.Value.TDDs -> PumpObject.TDDEntry - GetValue.Value.Alarms -> PumpObject.AlarmEntry - GetValue.Value.BasalProfiles -> PumpObject.BasalProfile - GetValue.Value.Version -> PumpObject.FirmwareEntry - GetValue.Value.BolusHistory, GetValue.Value.LatestBoluses -> PumpObject.BolusEntry - GetValue.Value.LatestTemporaryBasals -> return null - GetValue.Value.WizardStatus -> return null - } - getValueResult.isSingleObject = when (value) { - GetValue.Value.StatusV1, GetValue.Value.StatusV2, GetValue.Value.Version -> true - else -> false - } + getValueResult.clear() + getValueResult.targetObject = when (value) { + GetValue.Value.StatusV1 -> PumpObject.StatusV1 + GetValue.Value.StatusV2 -> PumpObject.StatusV2 + GetValue.Value.TDDs -> PumpObject.TDDEntry + GetValue.Value.Alarms -> PumpObject.AlarmEntry + GetValue.Value.BasalProfiles -> PumpObject.BasalProfile + GetValue.Value.Version -> PumpObject.FirmwareEntry + GetValue.Value.BolusHistory, GetValue.Value.LatestBoluses -> PumpObject.BolusEntry + GetValue.Value.LatestTemporaryBasals -> return null + GetValue.Value.WizardStatus -> return null + } + getValueResult.isSingleObject = when (value) { + GetValue.Value.StatusV1, GetValue.Value.StatusV2, GetValue.Value.Version -> true + else -> false + } - apexBluetooth.send(GetValue(apexDeviceInfo, value)) - try { - aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Waiting for response") - getValueResult.waitMillis(if (getValueResult.isSingleObject) 5000 else 15000) - } catch (e: InterruptedException) { - aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") - isGetThreadRunning = false - disconnect(true) - return null - } + apexBluetooth.send(GetValue(apexDeviceInfo, value)) + try { + aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Waiting for response") + getValueResult.waitMillis(if (getValueResult.isSingleObject) 5000 else 15000) + } catch (e: InterruptedException) { + aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") + isGetThreadRunning = false + return null + } - if (getValueResult.response == null) { - aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") - isGetThreadRunning = false - disconnect(true) - return null - } + if (getValueResult.response == null) { + aapsLogger.error(LTag.PUMPCOMM, "Get ${value.name} | Timed out") + isGetThreadRunning = false + return null + } - aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Completed") - getValueResult.response - }} + aapsLogger.debug(LTag.PUMPCOMM, "Get ${value.name} | Completed") + getValueResult.response + } - private fun executeWithResponse(command: DeviceCommand): CommandResponse? = synchronized(commandLock) { synchronized(commandResponse) { - if (connectionStatus != ApexBluetooth.Status.CONNECTED) { - aapsLogger.debug(LTag.PUMPCOMM, "$command | Error - pump is disconnected") - return null - } + private fun intExecuteWithResponse(command: DeviceCommand): CommandResponse? = synchronized(commandResponse) { + if (connectionStatus != ApexBluetooth.Status.CONNECTED) { + aapsLogger.debug(LTag.PUMPCOMM, "$command | Error - pump is disconnected") + return null + } - commandResponse.clear() - apexBluetooth.send(command) - try { - aapsLogger.debug(LTag.PUMPCOMM, "$command | Waiting for response") - commandResponse.waitMillis(5000) - } catch (e: InterruptedException) { - aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") - commandResponse.waiting = false - disconnect(true) - return null - } + commandResponse.clear() + apexBluetooth.send(command) + try { + aapsLogger.debug(LTag.PUMPCOMM, "$command | Waiting for response") + commandResponse.waitMillis(5000) + } catch (e: InterruptedException) { + aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") + commandResponse.waiting = false + return null + } - if (commandResponse.response == null) { - aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") - commandResponse.waiting = false - disconnect(true) - return null - } + if (commandResponse.response == null) { + aapsLogger.error(LTag.PUMPCOMM, "$command | Timed out") + commandResponse.waiting = false + return null + } + + aapsLogger.debug(LTag.PUMPCOMM, "$command | Completed") + commandResponse.response + } + + fun getValue(value: GetValue.Value): List? { + synchronized(commandLock) { + val firstTry = intGetValue(value) + if (firstTry != null || doNotReconnect || !connectionFinished) return@getValue firstTry + doNotReconnect = true + } + + disconnect(true) + if (!ensureConnected()) { + synchronized(commandLock) { doNotReconnect = false } + return null + } - aapsLogger.debug(LTag.PUMPCOMM, "$command | Completed") - commandResponse.response - }} + synchronized(commandLock) { + val final = intGetValue(value) + doNotReconnect = false + return@getValue final + } + } + + private fun executeWithResponse(command: DeviceCommand): CommandResponse? { + synchronized(commandLock) { + val firstTry = intExecuteWithResponse(command) + if (firstTry != null || doNotReconnect || !connectionFinished) return@executeWithResponse firstTry + doNotReconnect = true + } + + disconnect(true) + if (!ensureConnected()) { + synchronized(commandLock) { doNotReconnect = false } + return null + } + + synchronized(commandLock) { + val final = intExecuteWithResponse(command) + doNotReconnect = false + return@executeWithResponse final + } + } + + private fun ensureConnected(): Boolean { + var times = 0 + while (!connectionFinished && times < 50) { + aapsLogger.debug(LTag.PUMPCOMM, "Waiting for successful connection") + SystemClock.sleep(500) + times++ + } + return connectionFinished + } override fun onCreate() { super.onCreate() @@ -917,8 +965,10 @@ class ApexService: DaggerService(), ApexBluetoothCallback { fun disconnect(isReconnect: Boolean = false) { manualDisconnect = !isReconnect - if (apexBluetooth.status != ApexBluetooth.Status.DISCONNECTED) apexBluetooth.disconnect() - if (isReconnect) apexBluetooth.connect() + if (apexBluetooth.status != ApexBluetooth.Status.DISCONNECTED) + apexBluetooth.disconnect() + else if (isReconnect) + apexBluetooth.connect() } @@ -928,9 +978,11 @@ class ApexService: DaggerService(), ApexBluetoothCallback { val version = getValue(GetValue.Value.Version)?.firstOrNull() if (version !is Version) { aapsLogger.error(LTag.PUMPCOMM, "Failed to get version - disconnecting.") - return disconnect() + return disconnect(true) } + aapsLogger.debug(LTag.PUMPCOMM, version.toString()) + pump.firmwareVersion = version if (!version.isSupported(FIRST_SUPPORTED_PROTO, LAST_SUPPORTED_PROTO)) { @@ -944,10 +996,15 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } onVersion(version) - aapsLogger.debug(LTag.PUMPCOMM, "Protocol v${version.protocolMajor}.${version.protocolMinor}") - if (!syncDateTime("BLE-onConnect")) return - if (!notifyAboutConnection("BLE-onConnect")) return + if (!syncDateTime("BLE-onConnect")) { + aapsLogger.error(LTag.PUMPCOMM, "Failed to sync date and time - disconnecting.") + return disconnect(true) + } + if (!notifyAboutConnection("BLE-onConnect")) { + aapsLogger.error(LTag.PUMPCOMM, "Failed to notify about connection - disconnecting.") + return disconnect(true) + } if (apexDeviceInfo.serialNumber != preferences.get(ApexStringKey.LastConnectedSerialNumber)) { onInitialConnection() @@ -955,13 +1012,20 @@ class ApexService: DaggerService(), ApexBluetoothCallback { } rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) - if (!getStatus("BLE-onConnect")) return - if (!getBoluses("BLE-onConnect")) return + if (!getStatus("BLE-onConnect")) { + aapsLogger.error(LTag.PUMPCOMM, "Failed to get status - disconnecting.") + return disconnect(true) + } + if (!getBoluses("BLE-onConnect")) { + aapsLogger.error(LTag.PUMPCOMM, "Failed to get boluses - disconnecting.") + return disconnect(true) + } unreachableTimerTask?.cancel() unreachableTimerTask = null rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED)) pump.gettingReady = false + connectionFinished = true } private var isDisconnectLoopRunning = false @@ -989,6 +1053,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { override fun onDisconnect() { aapsLogger.debug(LTag.PUMPCOMM, "onDisconnect") + connectionFinished = false isGetThreadRunning = false synchronized(getValueResult) { @@ -1004,7 +1069,7 @@ class ApexService: DaggerService(), ApexBluetoothCallback { lastConnectedTimestamp = System.currentTimeMillis() if (unreachableTimerTask == null) - unreachableTimerTask = timer.schedule(60000) { + unreachableTimerTask = timer.schedule(120000) { uiInteraction.addNotification( Notification.PUMP_UNREACHABLE, rh.gs(R.string.error_pump_unreachable), From 30a08e89564ee15e5fb39faaa5a2a49298d359d5 Mon Sep 17 00:00:00 2001 From: Timur Koneev Date: Thu, 20 Mar 2025 19:07:11 +0300 Subject: [PATCH 28/29] apex: add pretty icon to the fragment --- .../main/res/drawable/ic_apex_detailed.xml | 10864 ++++++++++++++++ .../src/main/res/layout/apex_fragment.xml | 4 +- 2 files changed, 10866 insertions(+), 2 deletions(-) create mode 100644 pump/apex/src/main/res/drawable/ic_apex_detailed.xml diff --git a/pump/apex/src/main/res/drawable/ic_apex_detailed.xml b/pump/apex/src/main/res/drawable/ic_apex_detailed.xml new file mode 100644 index 000000000000..26688aa37aa2 --- /dev/null +++ b/pump/apex/src/main/res/drawable/ic_apex_detailed.xml @@ -0,0 +1,10864 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pump/apex/src/main/res/layout/apex_fragment.xml b/pump/apex/src/main/res/layout/apex_fragment.xml index 76cba50dd467..a9559ac23c7d 100644 --- a/pump/apex/src/main/res/layout/apex_fragment.xml +++ b/pump/apex/src/main/res/layout/apex_fragment.xml @@ -402,12 +402,12 @@ + android:src="@drawable/ic_apex_detailed"/> \ No newline at end of file From 4cd50550be1ec3cb689416ab6ec04e5d32a74d91 Mon Sep 17 00:00:00 2001 From: Roman Rihter Date: Thu, 20 Mar 2025 16:06:26 +0300 Subject: [PATCH 29/29] apex: fix exact battery percentage getString --- pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt index cfe39eb327ca..e1758839cbbd 100644 --- a/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt +++ b/pump/apex/src/main/kotlin/app/aaps/pump/apex/ApexPump.kt @@ -11,6 +11,7 @@ import app.aaps.pump.apex.connectivity.commands.pump.Version import org.joda.time.DateTime import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.roundToInt /** * @author Roman Rikhter (teledurak@gmail.com) @@ -217,7 +218,7 @@ class ApexPump @Inject constructor() { fun getBatteryLevel(rh: ResourceHelper): String = if (batteryLevel.voltage == null) rh.gs(R.string.overview_pump_battery_approximate, batteryLevel.percentage) else - rh.gs(R.string.overview_pump_battery_exact, batteryLevel.percentage, batteryLevel.voltage) + rh.gs(R.string.overview_pump_battery_exact, batteryLevel.percentage, (batteryLevel.voltage * 1000).roundToInt()) fun getReservoirLevel(rh: ResourceHelper): String = rh.gs(R.string.overview_pump_reservoir, reservoirLevel)