diff --git a/build.gradle b/build.gradle index cf4d289cfa..83722e0290 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ buildscript { plugins { id "io.gitlab.arturbosch.detekt" version "1.16.0-RC2" - id "org.jlleitschuh.gradle.ktlint" version "9.4.1" + id "org.jlleitschuh.gradle.ktlint" version "10.1.0" } allprojects { diff --git a/omnipod-common/src/main/res/values/strings.xml b/omnipod-common/src/main/res/values/strings.xml index eb3ae323c6..040757704d 100644 --- a/omnipod-common/src/main/res/values/strings.xml +++ b/omnipod-common/src/main/res/values/strings.xml @@ -13,7 +13,11 @@ AAPS.Omnipod.low_reservoir_alert_units AAPS.Omnipod.automatically_acknowledge_alerts_enabled common_preferences_category_alerts - + common_preferences_category_notifications_settings + AAPS.Omnipod.notification_uncertain_tbr_sound_enabled + AAPS.Omnipod.notification_uncertain_smb_sound_enabled + AAPS.Omnipod.notification_uncertain_bolus_sound_enabled Pod Management Actions @@ -132,6 +136,11 @@ Other Alerts Confirmation Beeps + Notifications + Sound for uncertain TBR notifications enabled + Sound for + uncertain SMB notifications enabled + Sound for uncertain bolus notifications enabled No Active Pod diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 24370e0053..8ab1e04845 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -7,7 +7,9 @@ import dagger.android.HasAndroidInjector import info.nightscout.androidaps.activities.ErrorHelperActivity.Companion.runAlarm import info.nightscout.androidaps.data.DetailedBolusInfo import info.nightscout.androidaps.data.PumpEnactResult +import info.nightscout.androidaps.events.EventPreferenceChange import info.nightscout.androidaps.events.EventProfileSwitchChanged +import info.nightscout.androidaps.events.EventRefreshOverview import info.nightscout.androidaps.events.EventTempBasalChange import info.nightscout.androidaps.interfaces.* import info.nightscout.androidaps.logging.AAPSLogger @@ -25,10 +27,7 @@ import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BeepType -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.DeliveryStatus -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.PodConstants +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.ResponseType import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.CommandConfirmed import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager @@ -40,16 +39,24 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.OmnipodDashOvervi import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram import info.nightscout.androidaps.queue.commands.Command import info.nightscout.androidaps.queue.commands.CustomCommand +import info.nightscout.androidaps.utils.FabricPrivacy import info.nightscout.androidaps.utils.T import info.nightscout.androidaps.utils.TimeChangeType import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.androidaps.utils.sharedPreferences.SP import io.reactivex.Completable import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign import org.json.JSONObject +import java.time.Duration +import java.time.ZonedDateTime import java.util.* +import java.util.concurrent.CountDownLatch import javax.inject.Inject import javax.inject.Singleton +import kotlin.concurrent.thread import kotlin.math.ceil import kotlin.random.Random @@ -63,6 +70,8 @@ class OmnipodDashPumpPlugin @Inject constructor( private val pumpSync: PumpSync, private val rxBus: RxBusWrapper, private val context: Context, + private val aapsSchedulers: AapsSchedulers, + private val fabricPrivacy: FabricPrivacy, injector: HasAndroidInjector, aapsLogger: AAPSLogger, @@ -70,9 +79,13 @@ class OmnipodDashPumpPlugin @Inject constructor( commandQueue: CommandQueueProvider ) : PumpPluginBase(pluginDescription, injector, aapsLogger, resourceHelper, commandQueue), Pump { @Volatile var bolusCanceled = false + @Volatile var bolusDeliveryInProgress = false + private val handler: Handler = Handler(Looper.getMainLooper()) - lateinit private var statusChecker: Runnable - var nextPodWarningCheck : Long = 0 + private lateinit var statusChecker: Runnable + var nextPodWarningCheck: Long = 0 + @Volatile var stopConnecting: CountDownLatch? = null + private var disposables: CompositeDisposable = CompositeDisposable() companion object { private const val BOLUS_RETRY_INTERVAL_MS = 2000.toLong() @@ -95,10 +108,32 @@ class OmnipodDashPumpPlugin @Inject constructor( statusChecker = Runnable { refreshStatusOnUnacknowledgedCommands() updatePodWarnings() + // createFakeTBRWhenNoActivePod() + // TODO: this is called from the main thread handler.postDelayed(statusChecker, STATUS_CHECK_INTERVAL_MS) } } + private fun createFakeTBRWhenNoActivePod() { + if (!podStateManager.isPodRunning) { + val expectedState = pumpSync.expectedPumpState() + val tbr = expectedState.temporaryBasal + if (tbr == null || tbr.rate != 0.0) { + aapsLogger.info(LTag.PUMP, "createFakeTBRWhenNoActivePod") + pumpSync.syncTemporaryBasalWithPumpId( + timestamp = System.currentTimeMillis(), + rate = 0.0, + duration = T.mins(PodConstants.MAX_POD_LIFETIME.toMinutes()).msecs(), + isAbsolute = true, + type = PumpSync.TemporaryBasalType.PUMP_SUSPEND, + pumpId = Random.Default.nextLong(), // we don't use this, just make sure it's unique + pumpType = PumpType.OMNIPOD_DASH, + pumpSerial = serialNumber() + ) + } + } + } + private fun updatePodWarnings() { if (System.currentTimeMillis() > nextPodWarningCheck) { if (!podStateManager.isPodRunning) { @@ -121,7 +156,17 @@ class OmnipodDashPumpPlugin @Inject constructor( rxBus.send(EventNewNotification(notification)) } else { rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED)) - // TODO: time out of sync notification? + if (!podStateManager.sameTimeZone) { + val notification = + Notification( + Notification.OMNIPOD_TIME_OUT_OF_SYNC, + "Timezone on pod is different from the timezone on phone. " + + "Basal rate is incorrect" + + "Switch profile to fix", + Notification.NORMAL + ) + rxBus.send(EventNewNotification(notification)) + } } } nextPodWarningCheck = DateTimeUtil.getTimeInFutureFromMinutes(15) @@ -132,13 +177,13 @@ class OmnipodDashPumpPlugin @Inject constructor( if (podStateManager.isPodRunning && podStateManager.activeCommand != null && commandQueue.size() == 0 && - commandQueue.performing() == null) { + commandQueue.performing() == null + ) { commandQueue.readStatus("Unconfirmed command", null) } } override fun isInitialized(): Boolean { - // TODO return true } @@ -153,37 +198,64 @@ class OmnipodDashPumpPlugin @Inject constructor( override fun isConnected(): Boolean { - return true + return !podStateManager.isPodRunning || + podStateManager.bluetoothConnectionState == OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED } override fun isConnecting(): Boolean { - // TODO - return false + return stopConnecting != null } override fun isHandshakeInProgress(): Boolean { - // TODO - return false + return stopConnecting != null && + podStateManager.bluetoothConnectionState == OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED } override fun finishHandshaking() { - // TODO } override fun connect(reason: String) { - // empty on purpose + aapsLogger.info(LTag.PUMP, "connect reason=$reason") + podStateManager.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING + + synchronized(this) { + stopConnecting?.let { + aapsLogger.warn(LTag.PUMP, "Already connecting: $stopConnecting") + return + } + val stop = CountDownLatch(1) + stopConnecting = stop + } + + thread( + start = true, + name = "ConnectionThread", + ) { + try { + stopConnecting?.let { + val error = omnipodManager.connect(it).ignoreElements().blockingGet() + aapsLogger.info(LTag.PUMPCOMM, "connect error=$error") + } + } finally { + synchronized(this) { + stopConnecting = null + } + } + } } override fun disconnect(reason: String) { - // TODO + aapsLogger.info(LTag.PUMP, "disconnect reason=$reason") + stopConnecting?.let { it.countDown() } + omnipodManager.disconnect(false) } override fun stopConnecting() { - // TODO + aapsLogger.info(LTag.PUMP, "stopConnecting") + stopConnecting?.let { it.countDown() } + omnipodManager.disconnect(true) } - - override fun getPumpStatus(reason: String) { aapsLogger.debug(LTag.PUMP, "getPumpStatus reason=$reason") if (reason != "REQUESTED BY USER" && !podStateManager.isActivationCompleted) { @@ -226,7 +298,7 @@ class OmnipodDashPumpPlugin @Inject constructor( pumpSync.syncTemporaryBasalWithPumpId( timestamp = System.currentTimeMillis(), rate = 0.0, - duration = T.mins(PodConstants.MAX_POD_LIFETIME.standardMinutes).msecs(), + duration = T.mins(PodConstants.MAX_POD_LIFETIME.toMinutes()).msecs(), isAbsolute = true, type = PumpSync.TemporaryBasalType.PUMP_SUSPEND, pumpId = Random.Default.nextLong(), // we don't use this, just make sure it's unique @@ -279,8 +351,8 @@ class OmnipodDashPumpPlugin @Inject constructor( podStateManager.createActiveCommand(historyId, basalProgram = basalProgram) }, command = omnipodManager.setBasalProgram(basalProgram, hasBasalBeepEnabled()).ignoreElements(), - post = failWhenUnconfirmed(deliverySuspended), // mark as failed even if it worked OK and try again vs. mark ok and - // deny later + post = failWhenUnconfirmed(deliverySuspended), + // mark as failed even if it worked OK and try again vs. mark ok and deny later ).toPumpEnactResult() } @@ -305,6 +377,8 @@ class OmnipodDashPumpPlugin @Inject constructor( } Completable.error(java.lang.IllegalStateException("Command not confirmed")) } else { + showNotification(Notification.PROFILE_SET_OK, "Profile set OK", Notification.INFO, null) + Completable.complete() } } @@ -320,7 +394,7 @@ class OmnipodDashPumpPlugin @Inject constructor( .map { pumpSyncTempBasal( 0.0, - PodConstants.MAX_POD_LIFETIME.standardMinutes, + PodConstants.MAX_POD_LIFETIME.toMinutes(), PumpSync.TemporaryBasalType.PUMP_SUSPEND ) rxBus.send(EventTempBasalChange()) @@ -340,10 +414,38 @@ class OmnipodDashPumpPlugin @Inject constructor( super.onStart() podStateManager.onStart() handler.postDelayed(statusChecker, STATUS_CHECK_INTERVAL_MS) + disposables += rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(aapsSchedulers.main) + .subscribe( + { + if (it.isChanged( + resourceHelper, + R.string.key_omnipod_common_expiration_reminder_enabled + ) || + it.isChanged( + resourceHelper, + R.string.key_omnipod_common_expiration_reminder_hours_before_shutdown + ) || + it.isChanged( + resourceHelper, + R.string.key_omnipod_common_low_reservoir_alert_enabled + ) || + it.isChanged( + resourceHelper, + R.string.key_omnipod_common_low_reservoir_alert_units + ) + ) { + commandQueue.customCommand(CommandUpdateAlertConfiguration(), null) + } + }, + fabricPrivacy::logException + ) } override fun onStop() { super.onStop() + disposables.clear() handler.removeCallbacks(statusChecker) } @@ -363,7 +465,7 @@ class OmnipodDashPumpPlugin @Inject constructor( // TODO: what do we have to answer here if delivery is suspended? val running = podStateManager.basalProgram val equal = (mapProfileToBasalProgram(profile) == running) - aapsLogger.info(LTag.PUMP, "isThisProfileSet: $equal") + aapsLogger.info(LTag.PUMP, "set: $equal. profile=$profile, running=$running") return equal } @@ -400,7 +502,8 @@ class OmnipodDashPumpPlugin @Inject constructor( override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { try { - aapsLogger.info(LTag.PUMP, "Delivering treatment: $detailedBolusInfo") + bolusDeliveryInProgress = true + aapsLogger.info(LTag.PUMP, "Delivering treatment: $detailedBolusInfo $bolusCanceled") val beepsConfigurationKey = if (detailedBolusInfo.bolusType == DetailedBolusInfo.BolusType.SMB) R.string.key_omnipod_common_smb_beeps_enabled else @@ -469,9 +572,19 @@ class OmnipodDashPumpPlugin @Inject constructor( ) } else { if (podStateManager.activeCommand != null) { + val sound = if (sp.getBoolean( + R.string + .key_omnipod_common_notification_uncertain_bolus_sound_enabled, + true + ) + ) + R.raw.boluserror + else + 0 + showErrorDialog( "Bolus delivery status uncertain. Refresh pod status to confirm or deny.", - R.raw.boluserror + sound ) } } @@ -487,6 +600,7 @@ class OmnipodDashPumpPlugin @Inject constructor( return ret } finally { bolusCanceled = false + bolusDeliveryInProgress = false } } @@ -621,7 +735,9 @@ class OmnipodDashPumpPlugin @Inject constructor( override fun stopBolusDelivering() { aapsLogger.info(LTag.PUMP, "stopBolusDelivering called") - bolusCanceled = true + if (bolusDeliveryInProgress) { + bolusCanceled = true + } } override fun setTempBasalAbsolute( @@ -910,7 +1026,7 @@ class OmnipodDashPumpPlugin @Inject constructor( .map { pumpSyncTempBasal( 0.0, - PodConstants.MAX_POD_LIFETIME.standardMinutes, + PodConstants.MAX_POD_LIFETIME.toMinutes(), PumpSync.TemporaryBasalType.PUMP_SUSPEND ) } @@ -969,8 +1085,67 @@ class OmnipodDashPumpPlugin @Inject constructor( } private fun updateAlertConfiguration(): PumpEnactResult { - // TODO - return PumpEnactResult(injector).success(false).enacted(false).comment("NOT IMPLEMENTED") + + val expirationReminderEnabled = sp.getBoolean(R.string.key_omnipod_common_expiration_reminder_enabled, true) + val expirationHours = sp.getInt(R.string.key_omnipod_common_expiration_reminder_hours_before_shutdown, 7) + val lowReservoirAlertEnabled = sp.getBoolean(R.string.key_omnipod_common_low_reservoir_alert_enabled, true) + val lowReservoirAlertUnits = sp.getInt(R.string.key_omnipod_common_low_reservoir_alert_units, 10) + + if (!podStateManager.differentAlertSettings( + expirationReminderEnabled, + expirationHours, + lowReservoirAlertEnabled, + lowReservoirAlertUnits + ) + ) { + return PumpEnactResult(injector).success(true).enacted(false) + } + + val podLifeLeft = Duration.between(ZonedDateTime.now(), podStateManager.expiry) + val expiryAlertDelay = podLifeLeft.minus(Duration.ofHours(expirationHours.toLong())) + if (expiryAlertDelay.isNegative) { + aapsLogger.warn( + LTag.PUMPBTCOMM, + "updateAlertConfiguration negative " + + "expiryAlertDuration=$expiryAlertDelay" + ) + PumpEnactResult(injector).success(false).enacted(false) + } + val alerts = listOf( + AlertConfiguration( + AlertType.LOW_RESERVOIR, + enabled = lowReservoirAlertEnabled, + durationInMinutes = 0, + autoOff = false, + AlertTrigger.ReservoirVolumeTrigger((lowReservoirAlertUnits * 10).toShort()), + BeepType.FOUR_TIMES_BIP_BEEP, + BeepRepetitionType.XXX + ), + AlertConfiguration( + AlertType.USER_SET_EXPIRATION, + enabled = expirationReminderEnabled, + durationInMinutes = 0, + autoOff = false, + AlertTrigger.TimerTrigger( + expiryAlertDelay.toMinutes().toShort() + ), + BeepType.FOUR_TIMES_BIP_BEEP, + BeepRepetitionType.XXX2 + ) + ) + return executeProgrammingCommand( + historyEntry = history.createRecord(OmnipodCommandType.CONFIGURE_ALERTS), + command = omnipodManager.programAlerts(alerts).ignoreElements(), + post = podStateManager.updateExpirationAlertSettings( + expirationReminderEnabled, + expirationHours + ).andThen( + podStateManager.updateExpirationAlertSettings( + lowReservoirAlertEnabled, + lowReservoirAlertUnits + ) + ) + ).toPumpEnactResult() } private fun playTestBeep(): PumpEnactResult { @@ -1029,11 +1204,17 @@ class OmnipodDashPumpPlugin @Inject constructor( .map { handleCommandConfirmation(it) } .ignoreElement(), checkPodKaput(), + refreshOverview(), post, ) ) } + private fun refreshOverview(): Completable = Completable.defer { + rxBus.send(EventRefreshOverview("Dash command", false)) + Completable.complete() + } + private fun handleCommandConfirmation(confirmation: CommandConfirmed) { val command = confirmation.command val historyEntry = history.getById(command.historyId) @@ -1065,6 +1246,7 @@ class OmnipodDashPumpPlugin @Inject constructor( rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED)) } rxBus.send(EventDismissNotification(Notification.OMNIPOD_TBR_ALERTS)) + rxBus.send(EventDismissNotification(Notification.OMNIPOD_TIME_OUT_OF_SYNC)) } OmnipodCommandType.SET_BASAL_PROFILE -> { @@ -1086,6 +1268,7 @@ class OmnipodDashPumpPlugin @Inject constructor( rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED)) rxBus.send(EventDismissNotification(Notification.FAILED_UPDATE_PROFILE)) rxBus.send(EventDismissNotification(Notification.OMNIPOD_TBR_ALERTS)) + rxBus.send(EventDismissNotification(Notification.OMNIPOD_TIME_OUT_OF_SYNC)) } } @@ -1186,14 +1369,22 @@ class OmnipodDashPumpPlugin @Inject constructor( message, urgency ) - // TODO add back sound when we have options to disable it - /* - if (sound != null) { + if (sound != null && soundEnabledForNotificationType(id)) { notification.soundId = sound - }*/ + } rxBus.send(EventNewNotification(notification)) } + private fun soundEnabledForNotificationType(notificationType: Int): Boolean { + return when (notificationType) { + Notification.OMNIPOD_TBR_ALERTS -> + sp.getBoolean(R.string.key_omnipod_common_notification_uncertain_tbr_sound_enabled, true) + Notification.OMNIPOD_UNCERTAIN_SMB -> + sp.getBoolean(R.string.key_omnipod_common_notification_uncertain_smb_sound_enabled, true) + else -> true + } + } + private fun dismissNotification(id: Int) { rxBus.send(EventDismissNotification(id)) } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManager.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManager.kt index 0a5b743f94..718df1597b 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManager.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManager.kt @@ -9,12 +9,13 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definitio import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.ResponseType import io.reactivex.Observable import java.util.* +import java.util.concurrent.CountDownLatch interface OmnipodDashManager { fun activatePodPart1(lowReservoirAlertTrigger: AlertTrigger.ReservoirVolumeTrigger?): Observable - fun activatePodPart2(basalProgram: BasalProgram): Observable + fun activatePodPart2(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?): Observable fun getStatus(type: ResponseType.StatusResponseType): Observable @@ -39,4 +40,8 @@ interface OmnipodDashManager { fun silenceAlerts(alertTypes: EnumSet): Observable fun deactivatePod(): Observable + + fun disconnect(closeGatt: Boolean = false) + + fun connect(stop: CountDownLatch): Observable } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt index 487ecf2c6c..e504e93e62 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt @@ -8,13 +8,17 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEven import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.GetVersionCommand.Companion.DEFAULT_UNIQUE_ID import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.* +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.PodConstants.Companion.MAX_POD_LIFETIME import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.utils.rx.AapsSchedulers import io.reactivex.Observable import io.reactivex.functions.Action import io.reactivex.functions.Consumer +import java.time.Duration +import java.time.ZonedDateTime import java.util.* +import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton @@ -74,6 +78,22 @@ class OmnipodDashManagerImpl @Inject constructor( } } + override fun disconnect(closeGatt: Boolean) { + bleManager.disconnect(closeGatt) + } + + override fun connect(stop: CountDownLatch): Observable { + return observeConnectToPodWithStop(stop) + .interceptPodEvents() + } + + private fun observeConnectToPodWithStop(stop: CountDownLatch): Observable { + return Observable.defer { + bleManager.connect(stop) + .doOnError { throwable -> logger.warn(LTag.PUMPBTCOMM, "observeConnectToPodWithStop error=$throwable") } + } + } + private val observeConnectToPod: Observable get() = Observable.defer { bleManager.connect() @@ -221,10 +241,7 @@ class OmnipodDashManagerImpl @Inject constructor( observeConnectToPod, observeActivationPart1Commands(lowReservoirAlertTrigger) ).doOnComplete(ActivationProgressUpdater(ActivationProgress.PHASE_1_COMPLETED)) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + .interceptPodEvents() } private fun observeActivationPart1Commands(lowReservoirAlertTrigger: AlertTrigger.ReservoirVolumeTrigger?): Observable { @@ -320,20 +337,19 @@ class OmnipodDashManagerImpl @Inject constructor( return observables.reversed() } - override fun activatePodPart2(basalProgram: BasalProgram): Observable { + override fun activatePodPart2(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?): + Observable { return Observable.concat( observePodReadyForActivationPart2, observeConnectToPod, - observeActivationPart2Commands(basalProgram) + observeActivationPart2Commands(basalProgram, userConfiguredExpirationHours) ).doOnComplete(ActivationProgressUpdater(ActivationProgress.COMPLETED)) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + .interceptPodEvents() } - private fun observeActivationPart2Commands(basalProgram: BasalProgram): Observable { - val observables = createActivationPart2Observables(basalProgram) + private fun observeActivationPart2Commands(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?): + Observable { + val observables = createActivationPart2Observables(basalProgram, userConfiguredExpirationHours) return if (observables.isEmpty()) { Observable.empty() @@ -342,7 +358,11 @@ class OmnipodDashManagerImpl @Inject constructor( } } - private fun createActivationPart2Observables(basalProgram: BasalProgram): List> { + private fun createActivationPart2Observables( + basalProgram: BasalProgram, + userConfiguredExpirationHours: Long? + ): + List> { val observables = ArrayList>() if (podStateManager.activationProgress.isBefore(ActivationProgress.CANNULA_INSERTED)) { @@ -368,33 +388,60 @@ class OmnipodDashManagerImpl @Inject constructor( ) } if (podStateManager.activationProgress.isBefore(ActivationProgress.UPDATED_EXPIRATION_ALERTS)) { + val podLifeLeft = Duration.between(ZonedDateTime.now(), podStateManager.expiry) + + val alerts = mutableListOf( + AlertConfiguration( + AlertType.EXPIRATION, + enabled = true, + durationInMinutes = TimeUnit.HOURS.toMinutes(7).toShort(), + autoOff = false, + AlertTrigger.TimerTrigger( + TimeUnit.HOURS.toMinutes(72).toShort() + ), // FIXME use activation time + BeepType.FOUR_TIMES_BIP_BEEP, + BeepRepetitionType.XXX3 + ), + AlertConfiguration( + AlertType.EXPIRATION_IMMINENT, + enabled = true, + durationInMinutes = 0, + autoOff = false, + AlertTrigger.TimerTrigger( + TimeUnit.HOURS.toMinutes(79).toShort() + ), // FIXME use activation time + BeepType.FOUR_TIMES_BIP_BEEP, + BeepRepetitionType.XXX4 + ) + ) + val userExpiryAlertDelay = podLifeLeft.minus( + Duration.ofHours(userConfiguredExpirationHours ?: MAX_POD_LIFETIME.toHours() + 1) + ) + if (userExpiryAlertDelay.isNegative) { + logger.warn( + LTag.PUMPBTCOMM, + "createActivationPart2Observables negative " + + "expiryAlertDuration=$userExpiryAlertDelay" + ) + } else { + alerts.add( + AlertConfiguration( + AlertType.USER_SET_EXPIRATION, + enabled = true, + durationInMinutes = 0, + autoOff = false, + AlertTrigger.TimerTrigger( + userExpiryAlertDelay.toMinutes().toShort() + ), + BeepType.FOUR_TIMES_BIP_BEEP, + BeepRepetitionType.XXX2 + ) + ) + } + observables.add( observeSendProgramAlertsCommand( - listOf( - // FIXME use user configured expiration alert - AlertConfiguration( - AlertType.EXPIRATION, - enabled = true, - durationInMinutes = TimeUnit.HOURS.toMinutes(7).toShort(), - autoOff = false, - AlertTrigger.TimerTrigger( - TimeUnit.HOURS.toMinutes(73).toShort() - ), // FIXME use activation time - BeepType.FOUR_TIMES_BIP_BEEP, - BeepRepetitionType.XXX3 - ), - AlertConfiguration( - AlertType.EXPIRATION_IMMINENT, - enabled = true, - durationInMinutes = TimeUnit.HOURS.toMinutes(1).toShort(), - autoOff = false, - AlertTrigger.TimerTrigger( - TimeUnit.HOURS.toMinutes(79).toShort() - ), // FIXME use activation time - BeepType.FOUR_TIMES_BIP_BEEP, - BeepRepetitionType.XXX4 - ) - ), + alerts, multiCommandFlag = true ).doOnComplete(ActivationProgressUpdater(ActivationProgress.UPDATED_EXPIRATION_ALERTS)) ) @@ -414,11 +461,7 @@ class OmnipodDashManagerImpl @Inject constructor( observeUniqueIdSet, observeConnectToPod, observeSendGetPodStatusCommand(type) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } override fun setBasalProgram(basalProgram: BasalProgram, hasBasalBeepEnabled: Boolean): Observable { @@ -426,11 +469,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendProgramBasalCommand(basalProgram, hasBasalBeepEnabled) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } private fun observeSendStopDeliveryCommand( @@ -461,11 +500,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.ALL, hasBasalBeepEnabled) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } override fun setTime(): Observable { @@ -495,11 +530,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendProgramTempBasalCommand(rate, durationInMinutes, tempBasalBeeps) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } override fun stopTempBasal(hasTempBasalBeepEnabled: Boolean): Observable { @@ -507,11 +538,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.TEMP_BASAL, hasTempBasalBeepEnabled) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } override fun bolus(units: Double, confirmationBeeps: Boolean, completionBeeps: Boolean): Observable { @@ -524,11 +551,7 @@ class OmnipodDashManagerImpl @Inject constructor( confirmationBeeps, completionBeeps ) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } override fun stopBolus(beep: Boolean): Observable { @@ -536,11 +559,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.BOLUS, beep) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } private fun observeSendConfigureBeepsCommand( @@ -569,11 +588,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendConfigureBeepsCommand(immediateBeepType = beepType) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } override fun programAlerts(alertConfigurations: List): Observable { @@ -581,11 +596,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendProgramAlertsCommand(alertConfigurations) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } private fun observeSendSilenceAlertsCommand(alertTypes: EnumSet): Observable { @@ -607,11 +618,7 @@ class OmnipodDashManagerImpl @Inject constructor( observePodRunning, observeConnectToPod, observeSendSilenceAlertsCommand(alertTypes) - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() } private val observeSendDeactivateCommand: Observable @@ -630,11 +637,7 @@ class OmnipodDashManagerImpl @Inject constructor( return Observable.concat( observeConnectToPod, observeSendDeactivateCommand - ) - // TODO these would be common for any observable returned in a public function in this class - .doOnNext(PodEventInterceptor()) - .doOnError(ErrorInterceptor()) - .subscribeOn(aapsSchedulers.io) + ).interceptPodEvents() // .doOnComplete(podStateManager::reset) } @@ -711,6 +714,12 @@ class OmnipodDashManagerImpl @Inject constructor( } } + private fun Observable.interceptPodEvents(): Observable { + return this.doOnNext(PodEventInterceptor()) + .doOnError(ErrorInterceptor()) + .subscribeOn(aapsSchedulers.io) + } + inner class ErrorInterceptor : Consumer { override fun accept(throwable: Throwable) { diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManager.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManager.kt index b2400713c7..7a98a61b19 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManager.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManager.kt @@ -1,10 +1,12 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.Connection import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.ConnectionState import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response import io.reactivex.Observable +import java.util.concurrent.CountDownLatch import kotlin.reflect.KClass interface OmnipodDashBleManager { @@ -13,9 +15,13 @@ interface OmnipodDashBleManager { fun getStatus(): ConnectionState - fun connect(): Observable + // used for sync connections + fun connect(timeoutMs: Long = Connection.BASE_CONNECT_TIMEOUT_MS * 3): Observable + + // used for async connections + fun connect(stopConnectionLatch: CountDownLatch): Observable fun pairNewPod(): Observable - fun disconnect() + fun disconnect(closeGatt: Boolean = false) } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt index 68a4f54960..60b9363238 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt @@ -16,6 +16,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.b import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import io.reactivex.Observable +import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Singleton @@ -83,7 +84,7 @@ class OmnipodDashBleManagerImpl @Inject constructor( } emitter.onComplete() } catch (ex: Exception) { - disconnect() + disconnect(false) emitter.tryOnError(ex) } finally { busy.set(false) @@ -100,56 +101,54 @@ class OmnipodDashBleManagerImpl @Inject constructor( return connection?.let { it.connectionState() } ?: NotConnected } - - override fun connect(): Observable = Observable.create { emitter -> - if (!busy.compareAndSet(false, true)) { - throw BusyException() - } - try { - emitter.onNext(PodEvent.BluetoothConnecting) - - val podAddress = - podState.bluetoothAddress - ?: throw FailedToConnectException("Missing bluetoothAddress, activate the pod first") - val podDevice = bluetoothAdapter.getRemoteDevice(podAddress) - val conn = connection - ?: Connection(podDevice, aapsLogger, context, podState) - connection = conn - if (conn.connectionState() is Connected && conn.session != null) { - emitter.onNext(PodEvent.AlreadyConnected(podAddress)) - emitter.onComplete() - return@create - } - - // two retries - for (i in 1..MAX_NUMBER_OF_CONNECTION_ATTEMPTS) { - try { - // wait i * CONNECTION_TIMEOUT - conn.connect(CONNECT_TIMEOUT_MULTIPLIER) - break - } catch (e: Exception) { - aapsLogger.warn(LTag.PUMPBTCOMM, "connect error=$e") - if (i == MAX_NUMBER_OF_CONNECTION_ATTEMPTS) { - emitter.onError(e) - return@create - } - } - } - - emitter.onNext(PodEvent.BluetoothConnected(podAddress)) - emitter.onNext(PodEvent.EstablishingSession) - establishSession(1.toByte()) - emitter.onNext(PodEvent.Connected) - - emitter.onComplete() - } catch (ex: Exception) { - disconnect() - emitter.tryOnError(ex) - } finally { - busy.set(false) - } + // used for sync connections + override fun connect(timeoutMs: Long): Observable { + return connect(ConnectionWaitCondition(timeoutMs = timeoutMs)) } + // used for async connections + override fun connect(stopConnectionLatch: CountDownLatch): Observable { + return connect(ConnectionWaitCondition(stopConnection = stopConnectionLatch)) + } + + private fun connect(connectionWaitCond: ConnectionWaitCondition): Observable = Observable + .create { + emitter -> + if (!busy.compareAndSet(false, true)) { + throw BusyException() + } + try { + emitter.onNext(PodEvent.BluetoothConnecting) + + val podAddress = + podState.bluetoothAddress + ?: throw FailedToConnectException("Missing bluetoothAddress, activate the pod first") + val podDevice = bluetoothAdapter.getRemoteDevice(podAddress) + val conn = connection + ?: Connection(podDevice, aapsLogger, context, podState) + connection = conn + if (conn.connectionState() is Connected && conn.session != null) { + emitter.onNext(PodEvent.AlreadyConnected(podAddress)) + emitter.onComplete() + return@create + } + + conn.connect(connectionWaitCond) + + emitter.onNext(PodEvent.BluetoothConnected(podAddress)) + emitter.onNext(PodEvent.EstablishingSession) + establishSession(1.toByte()) + emitter.onNext(PodEvent.Connected) + + emitter.onComplete() + } catch (ex: Exception) { + disconnect(false) + emitter.tryOnError(ex) + } finally { + busy.set(false) + } + } + private fun establishSession(msgSeq: Byte) { val conn = assertConnected() @@ -187,7 +186,6 @@ class OmnipodDashBleManagerImpl @Inject constructor( throw BusyException() } try { - if (podState.ltk != null) { emitter.onNext(PodEvent.AlreadyPaired) emitter.onComplete() @@ -207,12 +205,14 @@ class OmnipodDashBleManagerImpl @Inject constructor( val podDevice = bluetoothAdapter.getRemoteDevice(podAddress) val conn = Connection(podDevice, aapsLogger, context, podState) connection = conn + conn.connect(ConnectionWaitCondition(timeoutMs = 3 * Connection.BASE_CONNECT_TIMEOUT_MS)) emitter.onNext(PodEvent.BluetoothConnected(podAddress)) emitter.onNext(PodEvent.Pairing) + val mIO = conn.msgIO ?: throw ConnectException("Connection lost") val ltkExchanger = LTKExchanger( aapsLogger, - conn.msgIO, + mIO, ids, ) val pairResult = ltkExchanger.negotiateLTK() @@ -221,27 +221,24 @@ class OmnipodDashBleManagerImpl @Inject constructor( if (BuildConfig.DEBUG) { aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${pairResult.ltk.toHex()}") } - emitter.onNext(PodEvent.EstablishingSession) establishSession(pairResult.msgSeq) emitter.onNext(PodEvent.Connected) emitter.onComplete() } catch (ex: Exception) { - disconnect() + disconnect(false) emitter.tryOnError(ex) } finally { busy.set(false) } } - override fun disconnect() { - connection?.disconnect() + override fun disconnect(closeGatt: Boolean) { + connection?.disconnect(closeGatt) ?: aapsLogger.info(LTag.PUMPBTCOMM, "Trying to disconnect a null connection") } companion object { - const val MAX_NUMBER_OF_CONNECTION_ATTEMPTS = 2 const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else. - private const val CONNECT_TIMEOUT_MULTIPLIER = 4 } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/ServiceDiscoverer.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/ServiceDiscoverer.kt index 664db805e0..2fc1c42704 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/ServiceDiscoverer.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/ServiceDiscoverer.kt @@ -7,26 +7,40 @@ import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ConnectException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.Connected +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.Connection +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.Connection.Companion.STOP_CONNECTING_CHECK_INTERVAL_MS +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.ConnectionWaitCondition import java.math.BigInteger import java.util.* class ServiceDiscoverer( private val logger: AAPSLogger, private val gatt: BluetoothGatt, - private val bleCallbacks: BleCommCallbacks + private val bleCallbacks: BleCommCallbacks, + private val connection: Connection ) { /*** * This is first step after connection establishment */ - fun discoverServices(): Map { + fun discoverServices(connectionWaitCond: ConnectionWaitCondition): Map { logger.debug(LTag.PUMPBTCOMM, "Discovering services") bleCallbacks.startServiceDiscovery() val discover = gatt.discoverServices() if (!discover) { throw ConnectException("Could not start discovering services`") } - bleCallbacks.waitForServiceDiscovery(DISCOVER_SERVICES_TIMEOUT_MS) + connectionWaitCond.timeoutMs?.let { + bleCallbacks.waitForServiceDiscovery(it) + } + connectionWaitCond.stopConnection?.let { + while (!bleCallbacks.waitForServiceDiscovery(STOP_CONNECTING_CHECK_INTERVAL_MS)) { + if (it.count == 0L || connection.connectionState() !is Connected) { + throw ConnectException("stopConnecting called") + } + } + } logger.debug(LTag.PUMPBTCOMM, "Services discovered") val service = gatt.getService(SERVICE_UUID.toUuid()) ?: throw ConnectException("Service not found: $SERVICE_UUID") @@ -34,11 +48,10 @@ class ServiceDiscoverer( ?: throw ConnectException("Characteristic not found: ${CharacteristicType.CMD.value}") val dataChar = service.getCharacteristic(CharacteristicType.DATA.uuid) ?: throw ConnectException("Characteristic not found: ${CharacteristicType.DATA.value}") - var chars = mapOf( + return mapOf( CharacteristicType.CMD to cmdChar, CharacteristicType.DATA to dataChar ) - return chars } private fun String.toUuid(): UUID = UUID( @@ -49,6 +62,5 @@ class ServiceDiscoverer( companion object { private const val SERVICE_UUID = "1a7e-4024-e3ed-4464-8b7e-751e03d0dc5f" - private const val DISCOVER_SERVICES_TIMEOUT_MS = 5000 } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/callbacks/BleCommCallbacks.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/callbacks/BleCommCallbacks.kt index 3d4d3d1339..6e193193b3 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/callbacks/BleCommCallbacks.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/callbacks/BleCommCallbacks.kt @@ -40,7 +40,8 @@ class BleCommCallbacks( if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { connected.countDown() } - if (newState == BluetoothProfile.STATE_DISCONNECTED) { + if (newState == BluetoothProfile.STATE_DISCONNECTED && status != BluetoothGatt.GATT_SUCCESS) { + // If status == SUCCESS, it means that we initiated the disconnect. disconnectHandler.onConnectionLost(status) } } @@ -53,24 +54,28 @@ class BleCommCallbacks( } } - fun waitForConnection(timeoutMs: Int) { + fun waitForConnection(timeoutMs: Long): Boolean { + val latch = connected try { - connected.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS) + latch.await(timeoutMs, TimeUnit.MILLISECONDS) } catch (e: InterruptedException) { aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for Connection") } + return latch.count == 0L } fun startServiceDiscovery() { serviceDiscoveryComplete = CountDownLatch(1) } - fun waitForServiceDiscovery(timeoutMs: Int) { + fun waitForServiceDiscovery(timeoutMs: Long): Boolean { + val latch = serviceDiscoveryComplete try { - serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS) + latch.await(timeoutMs, TimeUnit.MILLISECONDS) } catch (e: InterruptedException) { aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for ServiceDiscovery") } + return latch.count == 0L } fun confirmWrite(expectedPayload: ByteArray, expectedUUID: String, timeoutMs: Long): WriteConfirmation { @@ -206,6 +211,8 @@ class BleCommCallbacks( fun resetConnection() { aapsLogger.debug(LTag.PUMPBTCOMM, "Reset connection") + connected?.countDown() + serviceDiscoveryComplete?.countDown() connected = CountDownLatch(1) serviceDiscoveryComplete = CountDownLatch(1) flushConfirmationQueue() diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt index dbe1c24b88..a91d645339 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt @@ -11,6 +11,7 @@ import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.WriteConfirmationError import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.WriteConfirmationSuccess +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandRTS import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.* import java.util.concurrent.BlockingQueue import java.util.concurrent.TimeUnit @@ -84,12 +85,17 @@ open class BleIO( * Called before sending a new message. * The incoming queues should be empty, so we log when they are not. */ - fun flushIncomingQueue() { + open fun flushIncomingQueue(): Boolean { + var foundRTS = false do { val found = incomingPackets.poll()?.also { - aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: queue not empty, flushing: {${it.toHex()}") + aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: queue not empty, flushing: ${it.toHex()}") + if (it.isNotEmpty() && it[0] == BleCommandRTS.data[0]) { + foundRTS = true + } } } while (found != null) + return foundRTS } /** diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessageIO.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessageIO.kt index c18c2b33f1..0aa3033894 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessageIO.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessageIO.kt @@ -35,7 +35,12 @@ class MessageIO( @Suppress("ReturnCount") fun sendMessage(msg: MessagePacket): MessageSendResult { - cmdBleIO.flushIncomingQueue() + val foundRTS = cmdBleIO.flushIncomingQueue() + if (foundRTS) { + val msg = receiveMessage(false) + aapsLogger.warn(LTag.PUMPBTCOMM, "sendMessage received message=$msg") + throw IllegalStateException("Received message while trying to send") + } dataBleIO.flushIncomingQueue() val rtsSendResult = cmdBleIO.sendAndConfirmPacket(BleCommandRTS.data) @@ -85,11 +90,13 @@ class MessageIO( } @Suppress("ReturnCount") - fun receiveMessage(): MessagePacket? { - val expectRTS = cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS) - if (expectRTS !is BleConfirmSuccess) { - aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading RTS: $expectRTS") - return null + fun receiveMessage(readRTS: Boolean = true): MessagePacket? { + if (readRTS) { + val expectRTS = cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS) + if (expectRTS !is BleConfirmSuccess) { + aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading RTS: $expectRTS") + return null + } } val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandCTS.data) @@ -219,6 +226,6 @@ class MessageIO( companion object { private const val MAX_PACKET_READ_TRIES = 4 - private const val MESSAGE_READ_TIMEOUT_MS = 2500.toLong() + private const val MESSAGE_READ_TIMEOUT_MS = 5000.toLong() } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Connection.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Connection.kt index 98d4c9ff8c..261f42e0ea 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Connection.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Connection.kt @@ -5,6 +5,7 @@ import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.content.Context +import android.os.SystemClock import info.nightscout.androidaps.extensions.toHex import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.LTag @@ -13,105 +14,100 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Ids import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ServiceDiscoverer import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ConnectException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.FailedToConnectException -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleSendSuccess import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CmdBleIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.DataBleIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.IncomingPackets import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager +import java.lang.IllegalArgumentException +import java.util.concurrent.CountDownLatch sealed class ConnectionState +object Connecting : ConnectionState() object Connected : ConnectionState() object NotConnected : ConnectionState() +data class ConnectionWaitCondition(var timeoutMs: Long? = null, val stopConnection: CountDownLatch? = null) { + init { + if (timeoutMs == null && stopConnection == null) { + throw IllegalArgumentException("One of timeoutMs or stopConnection has to be non null") + } + if (timeoutMs != null && stopConnection != null) { + throw IllegalArgumentException("One of timeoutMs or stopConnection has to be null") + } + } +} + class Connection( private val podDevice: BluetoothDevice, private val aapsLogger: AAPSLogger, - context: Context, + private val context: Context, private val podState: OmnipodDashPodStateManager ) : DisconnectHandler { private val incomingPackets = IncomingPackets() private val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets, this) - private val gattConnection: BluetoothGatt + private var gattConnection: BluetoothGatt? = null private val bluetoothManager: BluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - // The session is Synchronized because we can lose the connection right when establishing it + @Volatile var session: Session? = null - @Synchronized get - @Synchronized set - private val cmdBleIO: CmdBleIO - private val dataBleIO: DataBleIO - init { - aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to ${podDevice.address}") + @Volatile + var msgIO: MessageIO? = null + fun connect(connectionWaitCond: ConnectionWaitCondition) { + aapsLogger.debug("Connecting connectionWaitCond=$connectionWaitCond") + + podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING val autoConnect = false - podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING - gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE) - // OnDisconnect can be called after this point!!! - val state = waitForConnection(2) - if (state !is Connected) { - podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED - throw FailedToConnectException(podDevice.address) - } - podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED - val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks) - val discoveredCharacteristics = discoverer.discoverServices() - cmdBleIO = CmdBleIO( - aapsLogger, - discoveredCharacteristics[CharacteristicType.CMD]!!, - incomingPackets - .cmdQueue, - gattConnection, - bleCommCallbacks - ) - dataBleIO = DataBleIO( - aapsLogger, - discoveredCharacteristics[CharacteristicType.DATA]!!, - incomingPackets - .dataQueue, - gattConnection, - bleCommCallbacks - ) - - val sendResult = cmdBleIO.hello() - if (sendResult !is BleSendSuccess) { - throw FailedToConnectException("Could not send HELLO command to ${podDevice.address}") - } - cmdBleIO.readyToRead() - dataBleIO.readyToRead() - } - - val msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO) - - fun connect(timeoutMultiplier: Int) { - if (session != null) { - disconnect() - } - aapsLogger.debug("Connecting") - podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING - - if (!gattConnection.connect()) { + val gatt = gattConnection + ?: podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE) + gattConnection = gatt + if (!gatt.connect()) { throw FailedToConnectException("connect() returned false") } - - if (waitForConnection(timeoutMultiplier) !is Connected) { + val before = SystemClock.elapsedRealtime() + if (waitForConnection(connectionWaitCond) !is Connected) { podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED throw FailedToConnectException(podDevice.address) } + val waitedMs = SystemClock.elapsedRealtime() - before + val timeoutMs = connectionWaitCond.timeoutMs + if (timeoutMs != null) { + var newTimeout = timeoutMs - waitedMs + if (newTimeout < MIN_DISCOVERY_TIMEOUT_MS) { + newTimeout = MIN_DISCOVERY_TIMEOUT_MS + } + connectionWaitCond.timeoutMs = newTimeout + } podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED - val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks) - val discovered = discoverer.discoverServices() - dataBleIO.characteristic = discovered[CharacteristicType.DATA]!! - cmdBleIO.characteristic = discovered[CharacteristicType.CMD]!! - + val discoverer = ServiceDiscoverer(aapsLogger, gatt, bleCommCallbacks, this) + val discovered = discoverer.discoverServices(connectionWaitCond) + val cmdBleIO = CmdBleIO( + aapsLogger, + discovered[CharacteristicType.CMD]!!, + incomingPackets + .cmdQueue, + gatt, + bleCommCallbacks + ) + val dataBleIO = DataBleIO( + aapsLogger, + discovered[CharacteristicType.DATA]!!, + incomingPackets + .dataQueue, + gatt, + bleCommCallbacks + ) + msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO) // val ret = gattConnection.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH) // aapsLogger.info(LTag.PUMPBTCOMM, "requestConnectionPriority: $ret") cmdBleIO.hello() @@ -119,18 +115,33 @@ class Connection( dataBleIO.readyToRead() } - fun disconnect() { - aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnecting") + fun disconnect(closeGatt: Boolean) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnecting closeGatt=$closeGatt") podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED - - gattConnection.disconnect() + if (closeGatt) { + gattConnection?.close() + gattConnection = null + } else { + gattConnection?.disconnect() + } bleCommCallbacks.resetConnection() session = null + msgIO = null } - private fun waitForConnection(timeoutMultiplier: Int): ConnectionState { + private fun waitForConnection(connectionWaitCond: ConnectionWaitCondition): ConnectionState { + aapsLogger.debug(LTag.PUMPBTCOMM, "waitForConnection connectionWaitCond=$connectionWaitCond") try { - bleCommCallbacks.waitForConnection(BASE_CONNECT_TIMEOUT_MS * timeoutMultiplier) + connectionWaitCond.timeoutMs?.let { + bleCommCallbacks.waitForConnection(it) + } + connectionWaitCond.stopConnection?.let { + while (!bleCommCallbacks.waitForConnection(STOP_CONNECTING_CHECK_INTERVAL_MS)) { + if (it.count == 0L) { + throw ConnectException("stopConnecting called") + } + } + } } catch (e: InterruptedException) { // We are still going to check if connection was successful aapsLogger.info(LTag.PUMPBTCOMM, "Interrupted while waiting for connection") @@ -148,7 +159,9 @@ class Connection( } fun establishSession(ltk: ByteArray, msgSeq: Byte, ids: Ids, eapSqn: ByteArray): EapSqn? { - val eapAkaExchanger = SessionEstablisher(aapsLogger, msgIO, ltk, eapSqn, ids, msgSeq) + val mIO = msgIO ?: throw ConnectException("Connection lost") + + val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn, ids, msgSeq) return when (val keys = eapAkaExchanger.negotiateSessionKeys()) { is SessionNegotiationResynchronization -> { if (BuildConfig.DEBUG) { @@ -168,7 +181,7 @@ class Connection( keys.nonce, keys.ck ) - session = Session(aapsLogger, msgIO, ids, sessionKeys = keys, enDecrypt = enDecrypt) + session = Session(aapsLogger, mIO, ids, sessionKeys = keys, enDecrypt = enDecrypt) null } } @@ -177,10 +190,12 @@ class Connection( // This will be called from a different thread !!! override fun onConnectionLost(status: Int) { aapsLogger.info(LTag.PUMPBTCOMM, "Lost connection with status: $status") - disconnect() + disconnect(false) } companion object { - private const val BASE_CONNECT_TIMEOUT_MS = 10000 + const val BASE_CONNECT_TIMEOUT_MS = 10000L + const val MIN_DISCOVERY_TIMEOUT_MS = 10000L + const val STOP_CONNECTING_CHECK_INTERVAL_MS = 500L } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/EapAkaAttribute.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/EapAkaAttribute.kt index 94379b181c..b81d5c1216 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/EapAkaAttribute.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/EapAkaAttribute.kt @@ -1,7 +1,6 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session import info.nightscout.androidaps.extensions.toHex -import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException import java.util.* diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Session.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Session.kt index 87fccc725e..4c4531689a 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Session.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/Session.kt @@ -11,7 +11,6 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message. import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response -import kotlin.reflect.KClass sealed class CommandSendResult object CommandSendSuccess : CommandSendResult() @@ -113,10 +112,10 @@ class Session( // TODO verify length - //val uniqueId = data.copyOfRange(0, 4) - //val lenghtAndSequenceNumber = data.copyOfRange(4, 6) + // val uniqueId = data.copyOfRange(0, 4) + // val lenghtAndSequenceNumber = data.copyOfRange(4, 6) val payload = data.copyOfRange(6, data.size - 2) - //val crc = data.copyOfRange(data.size - 2, data.size) + // val crc = data.copyOfRange(data.size - 2, data.size) // TODO validate uniqueId, sequenceNumber and crc diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BeepRepetitionType.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BeepRepetitionType.kt index 0668852456..d7a1a8f761 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BeepRepetitionType.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BeepRepetitionType.kt @@ -5,9 +5,9 @@ enum class BeepRepetitionType( val value: Byte ) { - XXX(0x01.toByte()), // Used in lump of coal alert - XXX2(0x03.toByte()), // Used in low reservoir alert - XXX3(0x05.toByte()), // Used in user pod expiration alert - XXX4(0x06.toByte()), // Used in pod expiration alert - XXX5(0x08.toByte()); // Used in imminent pod expiration alert + XXX(0x01.toByte()), // Used in lump of coal alert, LOW_RESERVOIR + XXX2(0x03.toByte()), // Used in USER_SET_EXPIRATION + XXX3(0x05.toByte()), // published system expiration alert + XXX4(0x06.toByte()), // Used in imminent pod expiration alert + XXX5(0x08.toByte()); // Lump of coal alert } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/PodConstants.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/PodConstants.kt index f52cc8fb5f..ae648dd656 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/PodConstants.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/PodConstants.kt @@ -1,9 +1,9 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition -import org.joda.time.Duration +import java.time.Duration class PodConstants { companion object { - val MAX_POD_LIFETIME = Duration.standardHours(80) + val MAX_POD_LIFETIME = Duration.ofMinutes(80) } } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt index ebf81c8de9..af833c4552 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManager.kt @@ -3,7 +3,6 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state import info.nightscout.androidaps.data.DetailedBolusInfo import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.DefaultStatusResponse @@ -11,12 +10,9 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response. import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.VersionResponse import io.reactivex.Completable import io.reactivex.Maybe -import io.reactivex.Observable import io.reactivex.Single -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Duration import java.io.Serializable +import java.time.ZonedDateTime import java.util.* sealed class CommandConfirmationFromState @@ -37,11 +33,12 @@ interface OmnipodDashPodStateManager { var bluetoothConnectionState: BluetoothConnectionState var timeZone: TimeZone + val sameTimeZone: Boolean // The TimeZone is the same on the phone and on the pod val lastUpdatedSystem: Long // System.currentTimeMillis() val lastStatusResponseReceived: Long - val time: DateTime? - val timeDrift: Duration? - val expiry: DateTime? + val time: ZonedDateTime? + val timeDrift: java.time.Duration? + val expiry: ZonedDateTime? val messageSequenceNumber: Short val sequenceNumberOfLastProgrammingCommand: Short? @@ -104,6 +101,9 @@ interface OmnipodDashPodStateManager { - after getPodStatus was successful(we have an up-to-date podStatus) */ fun recoverActivationFromPodStatus(): String? + fun differentAlertSettings(expirationReminderEnabled: Boolean, expirationHours: Int, lowReservoirAlertEnabled: Boolean, lowReservoirAlertUnits: Int): Boolean + fun updateExpirationAlertSettings(expirationReminderEnabled: Boolean, expirationHours: Int): Completable + fun updateLowReservoirAlertSettings(lowReservoirAlertEnabled: Boolean, lowReservoirAlertUnits: Int): Completable data class ActiveCommand( val sequence: Short, diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt index 2656a6149c..ad9d789467 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/state/OmnipodDashPodStateManagerImpl.kt @@ -11,7 +11,6 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.R import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.EapSqn -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.DefaultStatusResponse @@ -20,12 +19,11 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response. import info.nightscout.androidaps.utils.sharedPreferences.SP import io.reactivex.Completable import io.reactivex.Maybe -import io.reactivex.Observable import io.reactivex.Single -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Duration import java.io.Serializable +import java.time.Duration +import java.time.Instant +import java.time.ZonedDateTime import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -107,6 +105,12 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( store() } + override val sameTimeZone: Boolean + get() { + val now = System.currentTimeMillis() + return TimeZone.getDefault().getOffset(now) == timeZone.getOffset(now) + } + override val bluetoothVersion: SoftwareVersion? get() = podState.bleVersion @@ -183,36 +187,40 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( override val lastStatusResponseReceived: Long get() = podState.lastStatusResponseReceived - override val time: DateTime? + override val time: ZonedDateTime? get() { val minutesSinceActivation = podState.minutesSinceActivation val activationTime = podState.activationTime if ((activationTime != null) && (minutesSinceActivation != null)) { - return DateTime(activationTime) - .plusMinutes(minutesSinceActivation.toInt()) - .plus(Duration(podState.lastUpdatedSystem, System.currentTimeMillis())) + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(activationTime), timeZone.toZoneId()) + .plusMinutes(minutesSinceActivation.toLong()) + .plus(Duration.ofMillis(System.currentTimeMillis() - lastUpdatedSystem)) } return null } override val timeDrift: Duration? get() { - return Duration(DateTime.now(), time) + return Duration.between(ZonedDateTime.now(), time) } - override val expiry: DateTime? - // TODO: Consider storing expiry datetime in pod state saving continuously recalculating to the same value + override val expiry: ZonedDateTime? get() { val podLifeInHours = podLifeInHours - val activationTime = podState.activationTime - if (podLifeInHours != null && activationTime != null) { - return DateTime(podState.activationTime).plusHours(podLifeInHours.toInt()) + val minutesSinceActivation = podState.minutesSinceActivation + if (podLifeInHours != null && minutesSinceActivation != null) { + return ZonedDateTime.now() + .plusHours(podLifeInHours.toLong()) + .minusMinutes(minutesSinceActivation.toLong()) + .plus(Duration.ofMillis(System.currentTimeMillis() - lastUpdatedSystem)) } return null } override var bluetoothConnectionState: OmnipodDashPodStateManager.BluetoothConnectionState + @Synchronized get() = podState.bluetoothConnectionState + @Synchronized set(bluetoothConnectionState) { podState.bluetoothConnectionState = bluetoothConnectionState rxBus.send(EventOmnipodDashPumpValuesChanged()) @@ -283,29 +291,29 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( requestedBolus: Double? ): Single { - return Single.create { source -> - if (activeCommand == null) { - val command = OmnipodDashPodStateManager.ActiveCommand( - podState.messageSequenceNumber, - createdRealtime = SystemClock.elapsedRealtime(), - historyId = historyId, - sendError = null, - basalProgram = basalProgram, - tempBasal = tempBasal, - requestedBolus = requestedBolus + return Single.create { source -> + if (activeCommand == null) { + val command = OmnipodDashPodStateManager.ActiveCommand( + podState.messageSequenceNumber, + createdRealtime = SystemClock.elapsedRealtime(), + historyId = historyId, + sendError = null, + basalProgram = basalProgram, + tempBasal = tempBasal, + requestedBolus = requestedBolus + ) + podState.activeCommand = command + source.onSuccess(command) + } else { + source.onError( + java.lang.IllegalStateException( + "Trying to send a command " + + "and the last command was not confirmed" ) - podState.activeCommand = command - source.onSuccess(command) - } else { - source.onError( - java.lang.IllegalStateException( - "Trying to send a command " + - "and the last command was not confirmed" - ) - ) - } + ) } } + } @Synchronized override fun observeNoActiveCommand(): Completable { @@ -391,6 +399,32 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( } } + override fun differentAlertSettings( + expirationReminderEnabled: Boolean, + expirationHours: Int, + lowReservoirAlertEnabled: Boolean, + lowReservoirAlertUnits: Int + ): Boolean { + return podState.expirationReminderEnabled == expirationReminderEnabled && + podState.expirationHours == expirationHours && + podState.lowReservoirAlertEnabled == lowReservoirAlertEnabled && + podState.lowReservoirAlertUnits == lowReservoirAlertUnits + } + + override fun updateExpirationAlertSettings(expirationReminderEnabled: Boolean, expirationHours: Int): + Completable = Completable.defer { + podState.expirationReminderEnabled = expirationReminderEnabled + podState.expirationHours = expirationHours + Completable.complete() + } + + override fun updateLowReservoirAlertSettings(lowReservoirAlertEnabled: Boolean, lowReservoirAlertUnits: Int): + Completable = Completable.defer { + podState.lowReservoirAlertEnabled = lowReservoirAlertEnabled + podState.lowReservoirAlertUnits = lowReservoirAlertUnits + Completable.complete() + } + @Synchronized override fun getCommandConfirmationFromState(): CommandConfirmationFromState { return podState.activeCommand?.run { @@ -429,7 +463,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( override fun onStart() { when (getCommandConfirmationFromState()) { CommandConfirmationSuccess, CommandConfirmationDenied -> { - val now = System.currentTimeMillis() + val now = SystemClock.elapsedRealtime() val newCommand = podState.activeCommand?.copy( createdRealtime = now, sentRealtime = now + 1 @@ -439,7 +473,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( } CommandSendingNotConfirmed -> { - val now = System.currentTimeMillis() + val now = SystemClock.elapsedRealtime() val newCommand = podState.activeCommand?.copy( createdRealtime = now, sentRealtime = now + 1 @@ -611,6 +645,11 @@ class OmnipodDashPodStateManagerImpl @Inject constructor( var firstPrimeBolusVolume: Short? = null var secondPrimeBolusVolume: Short? = null + var expirationReminderEnabled: Boolean? = null + var expirationHours: Int? = null + var lowReservoirAlertEnabled: Boolean? = null + var lowReservoirAlertUnits: Int? = null + var pulsesDelivered: Short? = null var pulsesRemaining: Short? = null var podStatus: PodStatus? = null diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt index 59e90ef7dc..97f70338a5 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt @@ -44,8 +44,8 @@ import info.nightscout.androidaps.utils.ui.UIRunnable import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign import org.apache.commons.lang3.StringUtils -import org.joda.time.DateTime -import org.joda.time.Duration +import java.time.Duration +import java.time.ZonedDateTime import java.util.* import javax.inject.Inject import kotlin.collections.ArrayList @@ -269,34 +269,41 @@ class OmnipodDashOverviewFragment : DaggerFragment() { // Update time on Pod podInfoBinding.timeOnPod.text = podStateManager.time?.let { - readableZonedTime(it) + resourceHelper.gs( + R.string.omnipod_common_time_with_timezone, + dateUtil.dateAndTimeString(it.toEpochSecond() * 1000), + podStateManager.timeZone.getDisplayName(true, TimeZone.SHORT) + ) } ?: PLACEHOLDER + val timeDeviationTooBig = podStateManager.timeDrift?.let { + Duration.ofMinutes(MAX_TIME_DEVIATION_MINUTES).minus( + it.abs() + ).isNegative + } ?: false podInfoBinding.timeOnPod.setTextColor( - podStateManager.timeDrift?.let { - if (it.abs().isLongerThan(Duration.standardMinutes(MAX_TIME_DEVIATION_MINUTES))) { - Color.RED - } else { + when { + !podStateManager.sameTimeZone -> + Color.MAGENTA + timeDeviationTooBig -> + Color.YELLOW + else -> Color.WHITE - } - } ?: Color.WHITE + } ) // Update Pod expiry time val expiresAt = podStateManager.expiry - if (expiresAt == null) { - podInfoBinding.podExpiryDate.text = PLACEHOLDER - podInfoBinding.podExpiryDate.setTextColor(Color.WHITE) - } else { - podInfoBinding.podExpiryDate.text = readableZonedTime(expiresAt) - podInfoBinding.podExpiryDate.setTextColor( - if (DateTime.now().isAfter(expiresAt)) { - Color.RED - } else { - Color.WHITE - } - ) + podInfoBinding.podExpiryDate.text = expiresAt?.let { + dateUtil.dateAndTimeString(it.toEpochSecond() * 1000) } + ?: PLACEHOLDER + podInfoBinding.podExpiryDate.setTextColor( + if (expiresAt != null && ZonedDateTime.now().isAfter(expiresAt)) + Color.RED + else + Color.WHITE + ) podStateManager.alarmType?.let { errors.add( @@ -371,14 +378,14 @@ class OmnipodDashOverviewFragment : DaggerFragment() { private fun updateLastConnection() { if (podStateManager.isUniqueIdSet) { podInfoBinding.lastConnection.text = readableDuration( - Duration( - podStateManager.lastUpdatedSystem, - System - .currentTimeMillis() + Duration.ofMillis( + System.currentTimeMillis() - + podStateManager.lastUpdatedSystem, + ) ) val lastConnectionColor = - if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().millis)) { + if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().toMillis())) { Color.RED } else { Color.WHITE @@ -425,12 +432,14 @@ class OmnipodDashOverviewFragment : DaggerFragment() { } } - val podStatusColor = - if (!podStateManager.isActivationCompleted || podStateManager.isPodKaput || podStateManager.isSuspended) { + val podStatusColor = when { + !podStateManager.isActivationCompleted || podStateManager.isPodKaput || podStateManager.isSuspended -> Color.RED - } else { + podStateManager.activeCommand != null -> + Color.YELLOW + else -> Color.WHITE - } + } podInfoBinding.podStatus.setTextColor(podStatusColor) } @@ -444,7 +453,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() { R.string.omnipod_common_overview_last_bolus_value, omnipodDashPumpPlugin.model().determineCorrectBolusSize(requestedBolus), resourceHelper.gs(R.string.insulin_unit_shortname), - readableDuration(Duration(it.createdRealtime, SystemClock.elapsedRealtime())) + readableDuration(Duration.ofMillis(SystemClock.elapsedRealtime() - it.createdRealtime)) ) text += " (uncertain) " textColor = Color.RED @@ -464,7 +473,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() { R.string.omnipod_common_overview_last_bolus_value, omnipodDashPumpPlugin.model().determineCorrectBolusSize(bolusSize), resourceHelper.gs(R.string.insulin_unit_shortname), - readableDuration(Duration(it.startTime, System.currentTimeMillis())) + readableDuration(Duration.ofMillis(System.currentTimeMillis() - it.startTime)) ) if (!it.deliveryComplete) { textColor = Color.YELLOW @@ -600,47 +609,10 @@ class OmnipodDashOverviewFragment : DaggerFragment() { } } - // private fun getTimeZone(): DateTimeZone { - // // return getSafe(() -> podState.getTimeZone()); - // return podStateManager.timeZone - // } - private fun getTimeZone(): TimeZone { - // Return timezone ID (e.g "Europe/Amsterdam") - return podStateManager.timeZone - } - - private fun readableZonedTime(time: DateTime): String { - val timeAsJavaData = time.toLocalDateTime().toDate() - return dateUtil.dateAndTimeString(timeAsJavaData.time) - - // // TODO: Handle timeZone ID - // val timeZone = getTimeZone() - // if (timeZone == "") { - // // No timezone defined, use local time (default) - // return dateUtil.dateAndTimeString(timeAsJavaData.time) - // } - // else { - // // Get full timezoned time - // val isDaylightTime = timeZone.inDaylightTime(timeAsJavaData) - // val locale = resources.configuration.locales.get(0) - // val timeZoneDisplayName = - // timeZone.getDisplayName(isDaylightTime, TimeZone.SHORT, locale) + " " + timeZone.getDisplayName( - // isDaylightTime, - // TimeZone.LONG, - // locale - // ) - // return resourceHelper.gs( - // R.string.omnipod_common_time_with_timezone, - // dateUtil.dateAndTimeString(timeAsJavaData.time), - // timeZoneDisplayName - // ) - // } - } - private fun readableDuration(duration: Duration): String { - val hours = duration.standardHours.toInt() - val minutes = duration.standardMinutes.toInt() - val seconds = duration.standardSeconds.toInt() + val hours = duration.toHours().toInt() + val minutes = duration.toMinutes().toInt() + val seconds = duration.seconds when { seconds < 10 -> { return resourceHelper.gs(R.string.omnipod_common_moments_ago) @@ -683,7 +655,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() { // FIXME ideally we should just have access to LocalAlertUtils here private fun getPumpUnreachableTimeout(): Duration { - return Duration.standardMinutes( + return Duration.ofMinutes( sp.getInt( R.string.key_pump_unreachable_threshold_minutes, Constants.DEFAULT_PUMP_UNREACHABLE_THRESHOLD_MINUTES diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt index 1a410acd49..4862dc6a46 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt @@ -9,6 +9,8 @@ import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.activati import info.nightscout.androidaps.plugins.pump.omnipod.dash.R import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.AlertTrigger +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager +import info.nightscout.androidaps.utils.sharedPreferences.SP import io.reactivex.Single import io.reactivex.rxkotlin.subscribeBy import javax.inject.Inject @@ -16,9 +18,10 @@ import javax.inject.Inject class DashInitializePodViewModel @Inject constructor( private val omnipodManager: OmnipodDashManager, injector: HasAndroidInjector, - logger: AAPSLogger + logger: AAPSLogger, + private val sp: SP, + private val podStateManager: OmnipodDashPodStateManager, ) : InitializePodViewModel(injector, logger) { - override fun isPodInAlarm(): Boolean = false // TODO override fun isPodActivationTimeExceeded(): Boolean = false // TODO @@ -27,8 +30,14 @@ class DashInitializePodViewModel @Inject constructor( override fun doExecuteAction(): Single = Single.create { source -> - // TODO use configured value for low reservoir trigger - val disposable = omnipodManager.activatePodPart1(AlertTrigger.ReservoirVolumeTrigger(200)).subscribeBy( + val lowReservoirAlertEnabled = sp.getBoolean(R.string.key_omnipod_common_low_reservoir_alert_enabled, true) + val lowReservoirAlertUnits = sp.getInt(R.string.key_omnipod_common_low_reservoir_alert_units, 10) + val lowReservoirAlertTrigger = if (lowReservoirAlertEnabled) { + AlertTrigger.ReservoirVolumeTrigger((lowReservoirAlertUnits * 10).toShort()) + } else + null + + val disposable = omnipodManager.activatePodPart1(lowReservoirAlertTrigger).subscribeBy( onNext = { podEvent -> logger.debug( LTag.PUMP, @@ -41,6 +50,7 @@ class DashInitializePodViewModel @Inject constructor( }, onComplete = { logger.debug("Pod activation part 1 completed") + podStateManager.updateLowReservoirAlertSettings(lowReservoirAlertEnabled, lowReservoirAlertUnits) source.onSuccess(PumpEnactResult(injector).success(true)) } ) diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInsertCannulaViewModel.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInsertCannulaViewModel.kt index 143fb8fb91..4173fcd4c7 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInsertCannulaViewModel.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInsertCannulaViewModel.kt @@ -8,12 +8,16 @@ import info.nightscout.androidaps.interfaces.ProfileFunction import info.nightscout.androidaps.interfaces.PumpSync import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.general.overview.events.EventDismissNotification +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification import info.nightscout.androidaps.plugins.pump.common.defs.PumpType import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.activation.viewmodel.action.InsertCannulaViewModel import info.nightscout.androidaps.plugins.pump.omnipod.dash.R import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram +import info.nightscout.androidaps.utils.sharedPreferences.SP import io.reactivex.Single import io.reactivex.rxkotlin.subscribeBy import javax.inject.Inject @@ -23,6 +27,9 @@ class DashInsertCannulaViewModel @Inject constructor( private val profileFunction: ProfileFunction, private val pumpSync: PumpSync, private val podStateManager: OmnipodDashPodStateManager, + private val rxBus: RxBusWrapper, + private val sp: SP, + injector: HasAndroidInjector, logger: AAPSLogger ) : InsertCannulaViewModel(injector, logger) { @@ -45,7 +52,15 @@ class DashInsertCannulaViewModel @Inject constructor( profile, basalProgram ) - val disposable = omnipodManager.activatePodPart2(basalProgram).subscribeBy( + val expirationReminderEnabled = sp.getBoolean(R.string.key_omnipod_common_expiration_reminder_enabled, true) + val expirationHours = sp.getInt(R.string.key_omnipod_common_expiration_reminder_hours_before_shutdown, 9) + + val expirationHoursBeforeShutdown = if (expirationReminderEnabled) + expirationHours.toLong() + else + null + + val disposable = omnipodManager.activatePodPart2(basalProgram, expirationHoursBeforeShutdown).subscribeBy( onNext = { podEvent -> logger.debug( LTag.PUMP, @@ -58,6 +73,7 @@ class DashInsertCannulaViewModel @Inject constructor( }, onComplete = { logger.debug("Pod activation part 2 completed") + podStateManager.basalProgram = basalProgram pumpSync.connectNewPump() pumpSync.insertTherapyEventIfNewWithTimestamp( timestamp = System.currentTimeMillis(), @@ -71,6 +87,14 @@ class DashInsertCannulaViewModel @Inject constructor( pumpType = PumpType.OMNIPOD_DASH, pumpSerial = podStateManager.uniqueId?.toString() ?: "n/a" ) + pumpSync.syncStopTemporaryBasalWithPumpId( + timestamp = System.currentTimeMillis(), + endPumpId = System.currentTimeMillis(), + pumpType = PumpType.OMNIPOD_DASH, + pumpSerial = podStateManager.uniqueId?.toString() ?: "n/a" + ) + podStateManager.updateExpirationAlertSettings(expirationReminderEnabled, expirationHours) + rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_NOT_ATTACHED)) source.onSuccess(PumpEnactResult(injector).success(true)) } ) diff --git a/omnipod-dash/src/main/res/xml/omnipod_dash_preferences.xml b/omnipod-dash/src/main/res/xml/omnipod_dash_preferences.xml index 26d400e602..0e1d2dd11a 100644 --- a/omnipod-dash/src/main/res/xml/omnipod_dash_preferences.xml +++ b/omnipod-dash/src/main/res/xml/omnipod_dash_preferences.xml @@ -65,11 +65,33 @@ validate:maxNumber="50" validate:minNumber="5" validate:testType="numericRange" /> + + + + + android:key="@string/key_omnipod_common_notification_uncertain_tbr_sound_enabled" + android:title="@string/omnipod_common_preferences_notification_uncertain_tbr_sound_enabled" /> + + + +