Merge pull request #56 from 0pen-dash/avereha/conn

avereha/conn
This commit is contained in:
Andrei Vereha 2021-07-28 19:21:05 +02:00 committed by GitHub
commit ea5ec8e2c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 749 additions and 420 deletions

View file

@ -63,7 +63,7 @@ buildscript {
plugins { plugins {
id "io.gitlab.arturbosch.detekt" version "1.16.0-RC2" 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 { allprojects {

View file

@ -13,7 +13,11 @@
<string name="key_omnipod_common_low_reservoir_alert_units" translatable="false">AAPS.Omnipod.low_reservoir_alert_units</string> <string name="key_omnipod_common_low_reservoir_alert_units" translatable="false">AAPS.Omnipod.low_reservoir_alert_units</string>
<string name="key_omnipod_common_automatically_silence_alerts_enabled" translatable="false">AAPS.Omnipod.automatically_acknowledge_alerts_enabled</string> <string name="key_omnipod_common_automatically_silence_alerts_enabled" translatable="false">AAPS.Omnipod.automatically_acknowledge_alerts_enabled</string>
<string name="key_common_preferences_category_alerts_settings" translatable="false">common_preferences_category_alerts</string> <string name="key_common_preferences_category_alerts_settings" translatable="false">common_preferences_category_alerts</string>
<string name="key_omnipod_common_preferences_category_notifications_settings"
translatable="false">common_preferences_category_notifications_settings</string>
<string name="key_omnipod_common_notification_uncertain_tbr_sound_enabled" translatable="false">AAPS.Omnipod.notification_uncertain_tbr_sound_enabled</string>
<string name="key_omnipod_common_notification_uncertain_smb_sound_enabled" translatable="false">AAPS.Omnipod.notification_uncertain_smb_sound_enabled</string>
<string name="key_omnipod_common_notification_uncertain_bolus_sound_enabled" translatable="false">AAPS.Omnipod.notification_uncertain_bolus_sound_enabled</string>
<!-- Omnipod - Pod Management --> <!-- Omnipod - Pod Management -->
<string name="omnipod_common_pod_management_title">Pod Management</string> <string name="omnipod_common_pod_management_title">Pod Management</string>
<string name="omnipod_common_pod_management_heading_actions">Actions</string> <string name="omnipod_common_pod_management_heading_actions">Actions</string>
@ -132,6 +136,11 @@
<string name="omnipod_common_preferences_category_other">Other</string> <string name="omnipod_common_preferences_category_other">Other</string>
<string name="omnipod_common_preferences_category_alerts">Alerts</string> <string name="omnipod_common_preferences_category_alerts">Alerts</string>
<string name="omnipod_common_preferences_category_confirmation_beeps">Confirmation Beeps</string> <string name="omnipod_common_preferences_category_confirmation_beeps">Confirmation Beeps</string>
<string name="omnipod_common_preferences_category_notifications">Notifications</string>
<string name="omnipod_common_preferences_notification_uncertain_tbr_sound_enabled">Sound for uncertain TBR notifications enabled</string>
<string name="omnipod_common_preferences_notification_uncertain_smb_sound_enabled">Sound for
uncertain SMB notifications enabled</string>
<string name="omnipod_common_preferences_notification_uncertain_bolus_sound_enabled">Sound for uncertain bolus notifications enabled</string>
<!-- Omnipod - Pod Status --> <!-- Omnipod - Pod Status -->
<string name="omnipod_common_pod_status_no_active_pod">No Active Pod</string> <string name="omnipod_common_pod_status_no_active_pod">No Active Pod</string>

View file

@ -7,7 +7,9 @@ import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.activities.ErrorHelperActivity.Companion.runAlarm import info.nightscout.androidaps.activities.ErrorHelperActivity.Companion.runAlarm
import info.nightscout.androidaps.data.DetailedBolusInfo import info.nightscout.androidaps.data.DetailedBolusInfo
import info.nightscout.androidaps.data.PumpEnactResult import info.nightscout.androidaps.data.PumpEnactResult
import info.nightscout.androidaps.events.EventPreferenceChange
import info.nightscout.androidaps.events.EventProfileSwitchChanged import info.nightscout.androidaps.events.EventProfileSwitchChanged
import info.nightscout.androidaps.events.EventRefreshOverview
import info.nightscout.androidaps.events.EventTempBasalChange import info.nightscout.androidaps.events.EventTempBasalChange
import info.nightscout.androidaps.interfaces.* import info.nightscout.androidaps.interfaces.*
import info.nightscout.androidaps.logging.AAPSLogger 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.definition.OmnipodCommandType
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.* 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.OmnipodDashManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.*
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.response.ResponseType 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.CommandConfirmed
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager 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.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram
import info.nightscout.androidaps.queue.commands.Command import info.nightscout.androidaps.queue.commands.Command
import info.nightscout.androidaps.queue.commands.CustomCommand import info.nightscout.androidaps.queue.commands.CustomCommand
import info.nightscout.androidaps.utils.FabricPrivacy
import info.nightscout.androidaps.utils.T import info.nightscout.androidaps.utils.T
import info.nightscout.androidaps.utils.TimeChangeType import info.nightscout.androidaps.utils.TimeChangeType
import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.rx.AapsSchedulers
import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import org.json.JSONObject import org.json.JSONObject
import java.time.Duration
import java.time.ZonedDateTime
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.concurrent.thread
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.random.Random import kotlin.random.Random
@ -63,6 +70,8 @@ class OmnipodDashPumpPlugin @Inject constructor(
private val pumpSync: PumpSync, private val pumpSync: PumpSync,
private val rxBus: RxBusWrapper, private val rxBus: RxBusWrapper,
private val context: Context, private val context: Context,
private val aapsSchedulers: AapsSchedulers,
private val fabricPrivacy: FabricPrivacy,
injector: HasAndroidInjector, injector: HasAndroidInjector,
aapsLogger: AAPSLogger, aapsLogger: AAPSLogger,
@ -70,9 +79,13 @@ class OmnipodDashPumpPlugin @Inject constructor(
commandQueue: CommandQueueProvider commandQueue: CommandQueueProvider
) : PumpPluginBase(pluginDescription, injector, aapsLogger, resourceHelper, commandQueue), Pump { ) : PumpPluginBase(pluginDescription, injector, aapsLogger, resourceHelper, commandQueue), Pump {
@Volatile var bolusCanceled = false @Volatile var bolusCanceled = false
@Volatile var bolusDeliveryInProgress = false
private val handler: Handler = Handler(Looper.getMainLooper()) private val handler: Handler = Handler(Looper.getMainLooper())
lateinit private var statusChecker: Runnable private lateinit var statusChecker: Runnable
var nextPodWarningCheck : Long = 0 var nextPodWarningCheck: Long = 0
@Volatile var stopConnecting: CountDownLatch? = null
private var disposables: CompositeDisposable = CompositeDisposable()
companion object { companion object {
private const val BOLUS_RETRY_INTERVAL_MS = 2000.toLong() private const val BOLUS_RETRY_INTERVAL_MS = 2000.toLong()
@ -95,10 +108,32 @@ class OmnipodDashPumpPlugin @Inject constructor(
statusChecker = Runnable { statusChecker = Runnable {
refreshStatusOnUnacknowledgedCommands() refreshStatusOnUnacknowledgedCommands()
updatePodWarnings() updatePodWarnings()
// createFakeTBRWhenNoActivePod()
// TODO: this is called from the main thread
handler.postDelayed(statusChecker, STATUS_CHECK_INTERVAL_MS) 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() { private fun updatePodWarnings() {
if (System.currentTimeMillis() > nextPodWarningCheck) { if (System.currentTimeMillis() > nextPodWarningCheck) {
if (!podStateManager.isPodRunning) { if (!podStateManager.isPodRunning) {
@ -121,7 +156,17 @@ class OmnipodDashPumpPlugin @Inject constructor(
rxBus.send(EventNewNotification(notification)) rxBus.send(EventNewNotification(notification))
} else { } else {
rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED)) 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) nextPodWarningCheck = DateTimeUtil.getTimeInFutureFromMinutes(15)
@ -132,13 +177,13 @@ class OmnipodDashPumpPlugin @Inject constructor(
if (podStateManager.isPodRunning && if (podStateManager.isPodRunning &&
podStateManager.activeCommand != null && podStateManager.activeCommand != null &&
commandQueue.size() == 0 && commandQueue.size() == 0 &&
commandQueue.performing() == null) { commandQueue.performing() == null
) {
commandQueue.readStatus("Unconfirmed command", null) commandQueue.readStatus("Unconfirmed command", null)
} }
} }
override fun isInitialized(): Boolean { override fun isInitialized(): Boolean {
// TODO
return true return true
} }
@ -153,37 +198,64 @@ class OmnipodDashPumpPlugin @Inject constructor(
override fun isConnected(): Boolean { override fun isConnected(): Boolean {
return true return !podStateManager.isPodRunning ||
podStateManager.bluetoothConnectionState == OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED
} }
override fun isConnecting(): Boolean { override fun isConnecting(): Boolean {
// TODO return stopConnecting != null
return false
} }
override fun isHandshakeInProgress(): Boolean { override fun isHandshakeInProgress(): Boolean {
// TODO return stopConnecting != null &&
return false podStateManager.bluetoothConnectionState == OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED
} }
override fun finishHandshaking() { override fun finishHandshaking() {
// TODO
} }
override fun connect(reason: String) { 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) { override fun disconnect(reason: String) {
// TODO aapsLogger.info(LTag.PUMP, "disconnect reason=$reason")
stopConnecting?.let { it.countDown() }
omnipodManager.disconnect(false)
} }
override fun stopConnecting() { override fun stopConnecting() {
// TODO aapsLogger.info(LTag.PUMP, "stopConnecting")
stopConnecting?.let { it.countDown() }
omnipodManager.disconnect(true)
} }
override fun getPumpStatus(reason: String) { override fun getPumpStatus(reason: String) {
aapsLogger.debug(LTag.PUMP, "getPumpStatus reason=$reason") aapsLogger.debug(LTag.PUMP, "getPumpStatus reason=$reason")
if (reason != "REQUESTED BY USER" && !podStateManager.isActivationCompleted) { if (reason != "REQUESTED BY USER" && !podStateManager.isActivationCompleted) {
@ -226,7 +298,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
pumpSync.syncTemporaryBasalWithPumpId( pumpSync.syncTemporaryBasalWithPumpId(
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
rate = 0.0, rate = 0.0,
duration = T.mins(PodConstants.MAX_POD_LIFETIME.standardMinutes).msecs(), duration = T.mins(PodConstants.MAX_POD_LIFETIME.toMinutes()).msecs(),
isAbsolute = true, isAbsolute = true,
type = PumpSync.TemporaryBasalType.PUMP_SUSPEND, type = PumpSync.TemporaryBasalType.PUMP_SUSPEND,
pumpId = Random.Default.nextLong(), // we don't use this, just make sure it's unique 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) podStateManager.createActiveCommand(historyId, basalProgram = basalProgram)
}, },
command = omnipodManager.setBasalProgram(basalProgram, hasBasalBeepEnabled()).ignoreElements(), command = omnipodManager.setBasalProgram(basalProgram, hasBasalBeepEnabled()).ignoreElements(),
post = failWhenUnconfirmed(deliverySuspended), // mark as failed even if it worked OK and try again vs. mark ok and post = failWhenUnconfirmed(deliverySuspended),
// deny later // mark as failed even if it worked OK and try again vs. mark ok and deny later
).toPumpEnactResult() ).toPumpEnactResult()
} }
@ -305,6 +377,8 @@ class OmnipodDashPumpPlugin @Inject constructor(
} }
Completable.error(java.lang.IllegalStateException("Command not confirmed")) Completable.error(java.lang.IllegalStateException("Command not confirmed"))
} else { } else {
showNotification(Notification.PROFILE_SET_OK, "Profile set OK", Notification.INFO, null)
Completable.complete() Completable.complete()
} }
} }
@ -320,7 +394,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
.map { .map {
pumpSyncTempBasal( pumpSyncTempBasal(
0.0, 0.0,
PodConstants.MAX_POD_LIFETIME.standardMinutes, PodConstants.MAX_POD_LIFETIME.toMinutes(),
PumpSync.TemporaryBasalType.PUMP_SUSPEND PumpSync.TemporaryBasalType.PUMP_SUSPEND
) )
rxBus.send(EventTempBasalChange()) rxBus.send(EventTempBasalChange())
@ -340,10 +414,38 @@ class OmnipodDashPumpPlugin @Inject constructor(
super.onStart() super.onStart()
podStateManager.onStart() podStateManager.onStart()
handler.postDelayed(statusChecker, STATUS_CHECK_INTERVAL_MS) 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() { override fun onStop() {
super.onStop() super.onStop()
disposables.clear()
handler.removeCallbacks(statusChecker) handler.removeCallbacks(statusChecker)
} }
@ -363,7 +465,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
// TODO: what do we have to answer here if delivery is suspended? // TODO: what do we have to answer here if delivery is suspended?
val running = podStateManager.basalProgram val running = podStateManager.basalProgram
val equal = (mapProfileToBasalProgram(profile) == running) val equal = (mapProfileToBasalProgram(profile) == running)
aapsLogger.info(LTag.PUMP, "isThisProfileSet: $equal") aapsLogger.info(LTag.PUMP, "set: $equal. profile=$profile, running=$running")
return equal return equal
} }
@ -400,7 +502,8 @@ class OmnipodDashPumpPlugin @Inject constructor(
override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult {
try { 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) val beepsConfigurationKey = if (detailedBolusInfo.bolusType == DetailedBolusInfo.BolusType.SMB)
R.string.key_omnipod_common_smb_beeps_enabled R.string.key_omnipod_common_smb_beeps_enabled
else else
@ -469,9 +572,19 @@ class OmnipodDashPumpPlugin @Inject constructor(
) )
} else { } else {
if (podStateManager.activeCommand != null) { 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( showErrorDialog(
"Bolus delivery status uncertain. Refresh pod status to confirm or deny.", "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 return ret
} finally { } finally {
bolusCanceled = false bolusCanceled = false
bolusDeliveryInProgress = false
} }
} }
@ -621,7 +735,9 @@ class OmnipodDashPumpPlugin @Inject constructor(
override fun stopBolusDelivering() { override fun stopBolusDelivering() {
aapsLogger.info(LTag.PUMP, "stopBolusDelivering called") aapsLogger.info(LTag.PUMP, "stopBolusDelivering called")
bolusCanceled = true if (bolusDeliveryInProgress) {
bolusCanceled = true
}
} }
override fun setTempBasalAbsolute( override fun setTempBasalAbsolute(
@ -910,7 +1026,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
.map { .map {
pumpSyncTempBasal( pumpSyncTempBasal(
0.0, 0.0,
PodConstants.MAX_POD_LIFETIME.standardMinutes, PodConstants.MAX_POD_LIFETIME.toMinutes(),
PumpSync.TemporaryBasalType.PUMP_SUSPEND PumpSync.TemporaryBasalType.PUMP_SUSPEND
) )
} }
@ -969,8 +1085,67 @@ class OmnipodDashPumpPlugin @Inject constructor(
} }
private fun updateAlertConfiguration(): PumpEnactResult { 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 { private fun playTestBeep(): PumpEnactResult {
@ -1029,11 +1204,17 @@ class OmnipodDashPumpPlugin @Inject constructor(
.map { handleCommandConfirmation(it) } .map { handleCommandConfirmation(it) }
.ignoreElement(), .ignoreElement(),
checkPodKaput(), checkPodKaput(),
refreshOverview(),
post, post,
) )
) )
} }
private fun refreshOverview(): Completable = Completable.defer {
rxBus.send(EventRefreshOverview("Dash command", false))
Completable.complete()
}
private fun handleCommandConfirmation(confirmation: CommandConfirmed) { private fun handleCommandConfirmation(confirmation: CommandConfirmed) {
val command = confirmation.command val command = confirmation.command
val historyEntry = history.getById(command.historyId) 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_POD_SUSPENDED))
} }
rxBus.send(EventDismissNotification(Notification.OMNIPOD_TBR_ALERTS)) rxBus.send(EventDismissNotification(Notification.OMNIPOD_TBR_ALERTS))
rxBus.send(EventDismissNotification(Notification.OMNIPOD_TIME_OUT_OF_SYNC))
} }
OmnipodCommandType.SET_BASAL_PROFILE -> { OmnipodCommandType.SET_BASAL_PROFILE -> {
@ -1086,6 +1268,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED)) rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED))
rxBus.send(EventDismissNotification(Notification.FAILED_UPDATE_PROFILE)) rxBus.send(EventDismissNotification(Notification.FAILED_UPDATE_PROFILE))
rxBus.send(EventDismissNotification(Notification.OMNIPOD_TBR_ALERTS)) 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, message,
urgency urgency
) )
// TODO add back sound when we have options to disable it if (sound != null && soundEnabledForNotificationType(id)) {
/*
if (sound != null) {
notification.soundId = sound notification.soundId = sound
}*/ }
rxBus.send(EventNewNotification(notification)) 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) { private fun dismissNotification(id: Int) {
rxBus.send(EventDismissNotification(id)) rxBus.send(EventDismissNotification(id))
} }

View file

@ -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 info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.ResponseType
import io.reactivex.Observable import io.reactivex.Observable
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch
interface OmnipodDashManager { interface OmnipodDashManager {
fun activatePodPart1(lowReservoirAlertTrigger: AlertTrigger.ReservoirVolumeTrigger?): Observable<PodEvent> fun activatePodPart1(lowReservoirAlertTrigger: AlertTrigger.ReservoirVolumeTrigger?): Observable<PodEvent>
fun activatePodPart2(basalProgram: BasalProgram): Observable<PodEvent> fun activatePodPart2(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?): Observable<PodEvent>
fun getStatus(type: ResponseType.StatusResponseType): Observable<PodEvent> fun getStatus(type: ResponseType.StatusResponseType): Observable<PodEvent>
@ -39,4 +40,8 @@ interface OmnipodDashManager {
fun silenceAlerts(alertTypes: EnumSet<AlertType>): Observable<PodEvent> fun silenceAlerts(alertTypes: EnumSet<AlertType>): Observable<PodEvent>
fun deactivatePod(): Observable<PodEvent> fun deactivatePod(): Observable<PodEvent>
fun disconnect(closeGatt: Boolean = false)
fun connect(stop: CountDownLatch): Observable<PodEvent>
} }

View file

@ -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.*
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.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.*
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.response.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.androidaps.utils.rx.AapsSchedulers
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.Action import io.reactivex.functions.Action
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
import java.time.Duration
import java.time.ZonedDateTime
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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<PodEvent> {
return observeConnectToPodWithStop(stop)
.interceptPodEvents()
}
private fun observeConnectToPodWithStop(stop: CountDownLatch): Observable<PodEvent> {
return Observable.defer {
bleManager.connect(stop)
.doOnError { throwable -> logger.warn(LTag.PUMPBTCOMM, "observeConnectToPodWithStop error=$throwable") }
}
}
private val observeConnectToPod: Observable<PodEvent> private val observeConnectToPod: Observable<PodEvent>
get() = Observable.defer { get() = Observable.defer {
bleManager.connect() bleManager.connect()
@ -221,10 +241,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observeConnectToPod, observeConnectToPod,
observeActivationPart1Commands(lowReservoirAlertTrigger) observeActivationPart1Commands(lowReservoirAlertTrigger)
).doOnComplete(ActivationProgressUpdater(ActivationProgress.PHASE_1_COMPLETED)) ).doOnComplete(ActivationProgressUpdater(ActivationProgress.PHASE_1_COMPLETED))
// TODO these would be common for any observable returned in a public function in this class .interceptPodEvents()
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
private fun observeActivationPart1Commands(lowReservoirAlertTrigger: AlertTrigger.ReservoirVolumeTrigger?): Observable<PodEvent> { private fun observeActivationPart1Commands(lowReservoirAlertTrigger: AlertTrigger.ReservoirVolumeTrigger?): Observable<PodEvent> {
@ -320,20 +337,19 @@ class OmnipodDashManagerImpl @Inject constructor(
return observables.reversed() return observables.reversed()
} }
override fun activatePodPart2(basalProgram: BasalProgram): Observable<PodEvent> { override fun activatePodPart2(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?):
Observable<PodEvent> {
return Observable.concat( return Observable.concat(
observePodReadyForActivationPart2, observePodReadyForActivationPart2,
observeConnectToPod, observeConnectToPod,
observeActivationPart2Commands(basalProgram) observeActivationPart2Commands(basalProgram, userConfiguredExpirationHours)
).doOnComplete(ActivationProgressUpdater(ActivationProgress.COMPLETED)) ).doOnComplete(ActivationProgressUpdater(ActivationProgress.COMPLETED))
// TODO these would be common for any observable returned in a public function in this class .interceptPodEvents()
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
private fun observeActivationPart2Commands(basalProgram: BasalProgram): Observable<PodEvent> { private fun observeActivationPart2Commands(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?):
val observables = createActivationPart2Observables(basalProgram) Observable<PodEvent> {
val observables = createActivationPart2Observables(basalProgram, userConfiguredExpirationHours)
return if (observables.isEmpty()) { return if (observables.isEmpty()) {
Observable.empty() Observable.empty()
@ -342,7 +358,11 @@ class OmnipodDashManagerImpl @Inject constructor(
} }
} }
private fun createActivationPart2Observables(basalProgram: BasalProgram): List<Observable<PodEvent>> { private fun createActivationPart2Observables(
basalProgram: BasalProgram,
userConfiguredExpirationHours: Long?
):
List<Observable<PodEvent>> {
val observables = ArrayList<Observable<PodEvent>>() val observables = ArrayList<Observable<PodEvent>>()
if (podStateManager.activationProgress.isBefore(ActivationProgress.CANNULA_INSERTED)) { if (podStateManager.activationProgress.isBefore(ActivationProgress.CANNULA_INSERTED)) {
@ -368,33 +388,60 @@ class OmnipodDashManagerImpl @Inject constructor(
) )
} }
if (podStateManager.activationProgress.isBefore(ActivationProgress.UPDATED_EXPIRATION_ALERTS)) { 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( observables.add(
observeSendProgramAlertsCommand( observeSendProgramAlertsCommand(
listOf( alerts,
// 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
)
),
multiCommandFlag = true multiCommandFlag = true
).doOnComplete(ActivationProgressUpdater(ActivationProgress.UPDATED_EXPIRATION_ALERTS)) ).doOnComplete(ActivationProgressUpdater(ActivationProgress.UPDATED_EXPIRATION_ALERTS))
) )
@ -414,11 +461,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observeUniqueIdSet, observeUniqueIdSet,
observeConnectToPod, observeConnectToPod,
observeSendGetPodStatusCommand(type) observeSendGetPodStatusCommand(type)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
override fun setBasalProgram(basalProgram: BasalProgram, hasBasalBeepEnabled: Boolean): Observable<PodEvent> { override fun setBasalProgram(basalProgram: BasalProgram, hasBasalBeepEnabled: Boolean): Observable<PodEvent> {
@ -426,11 +469,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendProgramBasalCommand(basalProgram, hasBasalBeepEnabled) observeSendProgramBasalCommand(basalProgram, hasBasalBeepEnabled)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
private fun observeSendStopDeliveryCommand( private fun observeSendStopDeliveryCommand(
@ -461,11 +500,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.ALL, hasBasalBeepEnabled) observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.ALL, hasBasalBeepEnabled)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
override fun setTime(): Observable<PodEvent> { override fun setTime(): Observable<PodEvent> {
@ -495,11 +530,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendProgramTempBasalCommand(rate, durationInMinutes, tempBasalBeeps) observeSendProgramTempBasalCommand(rate, durationInMinutes, tempBasalBeeps)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
override fun stopTempBasal(hasTempBasalBeepEnabled: Boolean): Observable<PodEvent> { override fun stopTempBasal(hasTempBasalBeepEnabled: Boolean): Observable<PodEvent> {
@ -507,11 +538,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.TEMP_BASAL, hasTempBasalBeepEnabled) observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.TEMP_BASAL, hasTempBasalBeepEnabled)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
override fun bolus(units: Double, confirmationBeeps: Boolean, completionBeeps: Boolean): Observable<PodEvent> { override fun bolus(units: Double, confirmationBeeps: Boolean, completionBeeps: Boolean): Observable<PodEvent> {
@ -524,11 +551,7 @@ class OmnipodDashManagerImpl @Inject constructor(
confirmationBeeps, confirmationBeeps,
completionBeeps completionBeeps
) )
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
override fun stopBolus(beep: Boolean): Observable<PodEvent> { override fun stopBolus(beep: Boolean): Observable<PodEvent> {
@ -536,11 +559,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.BOLUS, beep) observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.BOLUS, beep)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
private fun observeSendConfigureBeepsCommand( private fun observeSendConfigureBeepsCommand(
@ -569,11 +588,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendConfigureBeepsCommand(immediateBeepType = beepType) observeSendConfigureBeepsCommand(immediateBeepType = beepType)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
override fun programAlerts(alertConfigurations: List<AlertConfiguration>): Observable<PodEvent> { override fun programAlerts(alertConfigurations: List<AlertConfiguration>): Observable<PodEvent> {
@ -581,11 +596,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendProgramAlertsCommand(alertConfigurations) observeSendProgramAlertsCommand(alertConfigurations)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
private fun observeSendSilenceAlertsCommand(alertTypes: EnumSet<AlertType>): Observable<PodEvent> { private fun observeSendSilenceAlertsCommand(alertTypes: EnumSet<AlertType>): Observable<PodEvent> {
@ -607,11 +618,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning, observePodRunning,
observeConnectToPod, observeConnectToPod,
observeSendSilenceAlertsCommand(alertTypes) observeSendSilenceAlertsCommand(alertTypes)
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
} }
private val observeSendDeactivateCommand: Observable<PodEvent> private val observeSendDeactivateCommand: Observable<PodEvent>
@ -630,11 +637,7 @@ class OmnipodDashManagerImpl @Inject constructor(
return Observable.concat( return Observable.concat(
observeConnectToPod, observeConnectToPod,
observeSendDeactivateCommand observeSendDeactivateCommand
) ).interceptPodEvents()
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
// //
.doOnComplete(podStateManager::reset) .doOnComplete(podStateManager::reset)
} }
@ -711,6 +714,12 @@ class OmnipodDashManagerImpl @Inject constructor(
} }
} }
private fun Observable<PodEvent>.interceptPodEvents(): Observable<PodEvent> {
return this.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
}
inner class ErrorInterceptor : Consumer<Throwable> { inner class ErrorInterceptor : Consumer<Throwable> {
override fun accept(throwable: Throwable) { override fun accept(throwable: Throwable) {

View file

@ -1,10 +1,12 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm 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.comm.session.ConnectionState
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent 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.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import io.reactivex.Observable import io.reactivex.Observable
import java.util.concurrent.CountDownLatch
import kotlin.reflect.KClass import kotlin.reflect.KClass
interface OmnipodDashBleManager { interface OmnipodDashBleManager {
@ -13,9 +15,13 @@ interface OmnipodDashBleManager {
fun getStatus(): ConnectionState fun getStatus(): ConnectionState
fun connect(): Observable<PodEvent> // used for sync connections
fun connect(timeoutMs: Long = Connection.BASE_CONNECT_TIMEOUT_MS * 3): Observable<PodEvent>
// used for async connections
fun connect(stopConnectionLatch: CountDownLatch): Observable<PodEvent>
fun pairNewPod(): Observable<PodEvent> fun pairNewPod(): Observable<PodEvent>
fun disconnect() fun disconnect(closeGatt: Boolean = false)
} }

View file

@ -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.response.Response
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import io.reactivex.Observable import io.reactivex.Observable
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -83,7 +84,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(
} }
emitter.onComplete() emitter.onComplete()
} catch (ex: Exception) { } catch (ex: Exception) {
disconnect() disconnect(false)
emitter.tryOnError(ex) emitter.tryOnError(ex)
} finally { } finally {
busy.set(false) busy.set(false)
@ -100,56 +101,54 @@ class OmnipodDashBleManagerImpl @Inject constructor(
return connection?.let { it.connectionState() } return connection?.let { it.connectionState() }
?: NotConnected ?: NotConnected
} }
// used for sync connections
override fun connect(): Observable<PodEvent> = Observable.create { emitter -> override fun connect(timeoutMs: Long): Observable<PodEvent> {
if (!busy.compareAndSet(false, true)) { return connect(ConnectionWaitCondition(timeoutMs = timeoutMs))
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 async connections
override fun connect(stopConnectionLatch: CountDownLatch): Observable<PodEvent> {
return connect(ConnectionWaitCondition(stopConnection = stopConnectionLatch))
}
private fun connect(connectionWaitCond: ConnectionWaitCondition): Observable<PodEvent> = 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) { private fun establishSession(msgSeq: Byte) {
val conn = assertConnected() val conn = assertConnected()
@ -187,7 +186,6 @@ class OmnipodDashBleManagerImpl @Inject constructor(
throw BusyException() throw BusyException()
} }
try { try {
if (podState.ltk != null) { if (podState.ltk != null) {
emitter.onNext(PodEvent.AlreadyPaired) emitter.onNext(PodEvent.AlreadyPaired)
emitter.onComplete() emitter.onComplete()
@ -207,12 +205,14 @@ class OmnipodDashBleManagerImpl @Inject constructor(
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress) val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
val conn = Connection(podDevice, aapsLogger, context, podState) val conn = Connection(podDevice, aapsLogger, context, podState)
connection = conn connection = conn
conn.connect(ConnectionWaitCondition(timeoutMs = 3 * Connection.BASE_CONNECT_TIMEOUT_MS))
emitter.onNext(PodEvent.BluetoothConnected(podAddress)) emitter.onNext(PodEvent.BluetoothConnected(podAddress))
emitter.onNext(PodEvent.Pairing) emitter.onNext(PodEvent.Pairing)
val mIO = conn.msgIO ?: throw ConnectException("Connection lost")
val ltkExchanger = LTKExchanger( val ltkExchanger = LTKExchanger(
aapsLogger, aapsLogger,
conn.msgIO, mIO,
ids, ids,
) )
val pairResult = ltkExchanger.negotiateLTK() val pairResult = ltkExchanger.negotiateLTK()
@ -221,27 +221,24 @@ class OmnipodDashBleManagerImpl @Inject constructor(
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${pairResult.ltk.toHex()}") aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${pairResult.ltk.toHex()}")
} }
emitter.onNext(PodEvent.EstablishingSession) emitter.onNext(PodEvent.EstablishingSession)
establishSession(pairResult.msgSeq) establishSession(pairResult.msgSeq)
emitter.onNext(PodEvent.Connected) emitter.onNext(PodEvent.Connected)
emitter.onComplete() emitter.onComplete()
} catch (ex: Exception) { } catch (ex: Exception) {
disconnect() disconnect(false)
emitter.tryOnError(ex) emitter.tryOnError(ex)
} finally { } finally {
busy.set(false) busy.set(false)
} }
} }
override fun disconnect() { override fun disconnect(closeGatt: Boolean) {
connection?.disconnect() connection?.disconnect(closeGatt)
?: aapsLogger.info(LTag.PUMPBTCOMM, "Trying to disconnect a null connection") ?: aapsLogger.info(LTag.PUMPBTCOMM, "Trying to disconnect a null connection")
} }
companion object { companion object {
const val MAX_NUMBER_OF_CONNECTION_ATTEMPTS = 2
const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else. const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else.
private const val CONNECT_TIMEOUT_MULTIPLIER = 4
} }
} }

View file

@ -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.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ConnectException 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.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.math.BigInteger
import java.util.* import java.util.*
class ServiceDiscoverer( class ServiceDiscoverer(
private val logger: AAPSLogger, private val logger: AAPSLogger,
private val gatt: BluetoothGatt, private val gatt: BluetoothGatt,
private val bleCallbacks: BleCommCallbacks private val bleCallbacks: BleCommCallbacks,
private val connection: Connection
) { ) {
/*** /***
* This is first step after connection establishment * This is first step after connection establishment
*/ */
fun discoverServices(): Map<CharacteristicType, BluetoothGattCharacteristic> { fun discoverServices(connectionWaitCond: ConnectionWaitCondition): Map<CharacteristicType, BluetoothGattCharacteristic> {
logger.debug(LTag.PUMPBTCOMM, "Discovering services") logger.debug(LTag.PUMPBTCOMM, "Discovering services")
bleCallbacks.startServiceDiscovery() bleCallbacks.startServiceDiscovery()
val discover = gatt.discoverServices() val discover = gatt.discoverServices()
if (!discover) { if (!discover) {
throw ConnectException("Could not start discovering services`") 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") logger.debug(LTag.PUMPBTCOMM, "Services discovered")
val service = gatt.getService(SERVICE_UUID.toUuid()) val service = gatt.getService(SERVICE_UUID.toUuid())
?: throw ConnectException("Service not found: $SERVICE_UUID") ?: throw ConnectException("Service not found: $SERVICE_UUID")
@ -34,11 +48,10 @@ class ServiceDiscoverer(
?: throw ConnectException("Characteristic not found: ${CharacteristicType.CMD.value}") ?: throw ConnectException("Characteristic not found: ${CharacteristicType.CMD.value}")
val dataChar = service.getCharacteristic(CharacteristicType.DATA.uuid) val dataChar = service.getCharacteristic(CharacteristicType.DATA.uuid)
?: throw ConnectException("Characteristic not found: ${CharacteristicType.DATA.value}") ?: throw ConnectException("Characteristic not found: ${CharacteristicType.DATA.value}")
var chars = mapOf( return mapOf(
CharacteristicType.CMD to cmdChar, CharacteristicType.CMD to cmdChar,
CharacteristicType.DATA to dataChar CharacteristicType.DATA to dataChar
) )
return chars
} }
private fun String.toUuid(): UUID = UUID( private fun String.toUuid(): UUID = UUID(
@ -49,6 +62,5 @@ class ServiceDiscoverer(
companion object { companion object {
private const val SERVICE_UUID = "1a7e-4024-e3ed-4464-8b7e-751e03d0dc5f" private const val SERVICE_UUID = "1a7e-4024-e3ed-4464-8b7e-751e03d0dc5f"
private const val DISCOVER_SERVICES_TIMEOUT_MS = 5000
} }
} }

View file

@ -40,7 +40,8 @@ class BleCommCallbacks(
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
connected.countDown() 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) disconnectHandler.onConnectionLost(status)
} }
} }
@ -53,24 +54,28 @@ class BleCommCallbacks(
} }
} }
fun waitForConnection(timeoutMs: Int) { fun waitForConnection(timeoutMs: Long): Boolean {
val latch = connected
try { try {
connected.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS) latch.await(timeoutMs, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for Connection") aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for Connection")
} }
return latch.count == 0L
} }
fun startServiceDiscovery() { fun startServiceDiscovery() {
serviceDiscoveryComplete = CountDownLatch(1) serviceDiscoveryComplete = CountDownLatch(1)
} }
fun waitForServiceDiscovery(timeoutMs: Int) { fun waitForServiceDiscovery(timeoutMs: Long): Boolean {
val latch = serviceDiscoveryComplete
try { try {
serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS) latch.await(timeoutMs, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for ServiceDiscovery") aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for ServiceDiscovery")
} }
return latch.count == 0L
} }
fun confirmWrite(expectedPayload: ByteArray, expectedUUID: String, timeoutMs: Long): WriteConfirmation { fun confirmWrite(expectedPayload: ByteArray, expectedUUID: String, timeoutMs: Long): WriteConfirmation {
@ -206,6 +211,8 @@ class BleCommCallbacks(
fun resetConnection() { fun resetConnection() {
aapsLogger.debug(LTag.PUMPBTCOMM, "Reset connection") aapsLogger.debug(LTag.PUMPBTCOMM, "Reset connection")
connected?.countDown()
serviceDiscoveryComplete?.countDown()
connected = CountDownLatch(1) connected = CountDownLatch(1)
serviceDiscoveryComplete = CountDownLatch(1) serviceDiscoveryComplete = CountDownLatch(1)
flushConfirmationQueue() flushConfirmationQueue()

View file

@ -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.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.WriteConfirmationError 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.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 info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.*
import java.util.concurrent.BlockingQueue import java.util.concurrent.BlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -84,12 +85,17 @@ open class BleIO(
* Called before sending a new message. * Called before sending a new message.
* The incoming queues should be empty, so we log when they are not. * The incoming queues should be empty, so we log when they are not.
*/ */
fun flushIncomingQueue() { open fun flushIncomingQueue(): Boolean {
var foundRTS = false
do { do {
val found = incomingPackets.poll()?.also { 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) } while (found != null)
return foundRTS
} }
/** /**

View file

@ -35,7 +35,12 @@ class MessageIO(
@Suppress("ReturnCount") @Suppress("ReturnCount")
fun sendMessage(msg: MessagePacket): MessageSendResult { 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() dataBleIO.flushIncomingQueue()
val rtsSendResult = cmdBleIO.sendAndConfirmPacket(BleCommandRTS.data) val rtsSendResult = cmdBleIO.sendAndConfirmPacket(BleCommandRTS.data)
@ -85,11 +90,13 @@ class MessageIO(
} }
@Suppress("ReturnCount") @Suppress("ReturnCount")
fun receiveMessage(): MessagePacket? { fun receiveMessage(readRTS: Boolean = true): MessagePacket? {
val expectRTS = cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS) if (readRTS) {
if (expectRTS !is BleConfirmSuccess) { val expectRTS = cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS)
aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading RTS: $expectRTS") if (expectRTS !is BleConfirmSuccess) {
return null aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading RTS: $expectRTS")
return null
}
} }
val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandCTS.data) val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandCTS.data)
@ -219,6 +226,6 @@ class MessageIO(
companion object { companion object {
private const val MAX_PACKET_READ_TRIES = 4 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()
} }
} }

View file

@ -5,6 +5,7 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.content.Context import android.content.Context
import android.os.SystemClock
import info.nightscout.androidaps.extensions.toHex import info.nightscout.androidaps.extensions.toHex
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag 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.ServiceDiscoverer
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks 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.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.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.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CmdBleIO 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.DataBleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.IncomingPackets 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.comm.message.MessageIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import java.lang.IllegalArgumentException
import java.util.concurrent.CountDownLatch
sealed class ConnectionState sealed class ConnectionState
object Connecting : ConnectionState()
object Connected : ConnectionState() object Connected : ConnectionState()
object NotConnected : 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( class Connection(
private val podDevice: BluetoothDevice, private val podDevice: BluetoothDevice,
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
context: Context, private val context: Context,
private val podState: OmnipodDashPodStateManager private val podState: OmnipodDashPodStateManager
) : DisconnectHandler { ) : DisconnectHandler {
private val incomingPackets = IncomingPackets() private val incomingPackets = IncomingPackets()
private val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets, this) private val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets, this)
private val gattConnection: BluetoothGatt private var gattConnection: BluetoothGatt? = null
private val bluetoothManager: BluetoothManager = private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as 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 var session: Session? = null
@Synchronized get
@Synchronized set
private val cmdBleIO: CmdBleIO
private val dataBleIO: DataBleIO
init { @Volatile
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to ${podDevice.address}") var msgIO: MessageIO? = null
fun connect(connectionWaitCond: ConnectionWaitCondition) {
aapsLogger.debug("Connecting connectionWaitCond=$connectionWaitCond")
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING
val autoConnect = false val autoConnect = false
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING val gatt = gattConnection
gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE) ?: podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
// OnDisconnect can be called after this point!!! gattConnection = gatt
val state = waitForConnection(2) if (!gatt.connect()) {
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()) {
throw FailedToConnectException("connect() returned false") throw FailedToConnectException("connect() returned false")
} }
val before = SystemClock.elapsedRealtime()
if (waitForConnection(timeoutMultiplier) !is Connected) { if (waitForConnection(connectionWaitCond) !is Connected) {
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED
throw FailedToConnectException(podDevice.address) 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 podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED
val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks) val discoverer = ServiceDiscoverer(aapsLogger, gatt, bleCommCallbacks, this)
val discovered = discoverer.discoverServices() val discovered = discoverer.discoverServices(connectionWaitCond)
dataBleIO.characteristic = discovered[CharacteristicType.DATA]!! val cmdBleIO = CmdBleIO(
cmdBleIO.characteristic = discovered[CharacteristicType.CMD]!! 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) // val ret = gattConnection.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// aapsLogger.info(LTag.PUMPBTCOMM, "requestConnectionPriority: $ret") // aapsLogger.info(LTag.PUMPBTCOMM, "requestConnectionPriority: $ret")
cmdBleIO.hello() cmdBleIO.hello()
@ -119,18 +115,33 @@ class Connection(
dataBleIO.readyToRead() dataBleIO.readyToRead()
} }
fun disconnect() { fun disconnect(closeGatt: Boolean) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnecting") aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnecting closeGatt=$closeGatt")
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED
if (closeGatt) {
gattConnection.disconnect() gattConnection?.close()
gattConnection = null
} else {
gattConnection?.disconnect()
}
bleCommCallbacks.resetConnection() bleCommCallbacks.resetConnection()
session = null session = null
msgIO = null
} }
private fun waitForConnection(timeoutMultiplier: Int): ConnectionState { private fun waitForConnection(connectionWaitCond: ConnectionWaitCondition): ConnectionState {
aapsLogger.debug(LTag.PUMPBTCOMM, "waitForConnection connectionWaitCond=$connectionWaitCond")
try { 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) { } catch (e: InterruptedException) {
// We are still going to check if connection was successful // We are still going to check if connection was successful
aapsLogger.info(LTag.PUMPBTCOMM, "Interrupted while waiting for connection") 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? { 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()) { return when (val keys = eapAkaExchanger.negotiateSessionKeys()) {
is SessionNegotiationResynchronization -> { is SessionNegotiationResynchronization -> {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -168,7 +181,7 @@ class Connection(
keys.nonce, keys.nonce,
keys.ck keys.ck
) )
session = Session(aapsLogger, msgIO, ids, sessionKeys = keys, enDecrypt = enDecrypt) session = Session(aapsLogger, mIO, ids, sessionKeys = keys, enDecrypt = enDecrypt)
null null
} }
} }
@ -177,10 +190,12 @@ class Connection(
// This will be called from a different thread !!! // This will be called from a different thread !!!
override fun onConnectionLost(status: Int) { override fun onConnectionLost(status: Int) {
aapsLogger.info(LTag.PUMPBTCOMM, "Lost connection with status: $status") aapsLogger.info(LTag.PUMPBTCOMM, "Lost connection with status: $status")
disconnect() disconnect(false)
} }
companion object { 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
} }
} }

View file

@ -1,7 +1,6 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import info.nightscout.androidaps.extensions.toHex 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 info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
import java.util.* import java.util.*

View file

@ -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.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.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import kotlin.reflect.KClass
sealed class CommandSendResult sealed class CommandSendResult
object CommandSendSuccess : CommandSendResult() object CommandSendSuccess : CommandSendResult()
@ -113,10 +112,10 @@ class Session(
// TODO verify length // TODO verify length
//val uniqueId = data.copyOfRange(0, 4) // val uniqueId = data.copyOfRange(0, 4)
//val lenghtAndSequenceNumber = data.copyOfRange(4, 6) // val lenghtAndSequenceNumber = data.copyOfRange(4, 6)
val payload = data.copyOfRange(6, data.size - 2) 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 // TODO validate uniqueId, sequenceNumber and crc

View file

@ -5,9 +5,9 @@ enum class BeepRepetitionType(
val value: Byte val value: Byte
) { ) {
XXX(0x01.toByte()), // Used in lump of coal alert XXX(0x01.toByte()), // Used in lump of coal alert, LOW_RESERVOIR
XXX2(0x03.toByte()), // Used in low reservoir alert XXX2(0x03.toByte()), // Used in USER_SET_EXPIRATION
XXX3(0x05.toByte()), // Used in user pod expiration alert XXX3(0x05.toByte()), // published system expiration alert
XXX4(0x06.toByte()), // Used in pod expiration alert XXX4(0x06.toByte()), // Used in imminent pod expiration alert
XXX5(0x08.toByte()); // Used in imminent pod expiration alert XXX5(0x08.toByte()); // Lump of coal alert
} }

View file

@ -1,9 +1,9 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition
import org.joda.time.Duration import java.time.Duration
class PodConstants { class PodConstants {
companion object { companion object {
val MAX_POD_LIFETIME = Duration.standardHours(80) val MAX_POD_LIFETIME = Duration.ofMinutes(80)
} }
} }

View file

@ -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.data.DetailedBolusInfo
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id 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.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.definition.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.DefaultStatusResponse 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 info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.VersionResponse
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single 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.io.Serializable
import java.time.ZonedDateTime
import java.util.* import java.util.*
sealed class CommandConfirmationFromState sealed class CommandConfirmationFromState
@ -37,11 +33,12 @@ interface OmnipodDashPodStateManager {
var bluetoothConnectionState: BluetoothConnectionState var bluetoothConnectionState: BluetoothConnectionState
var timeZone: TimeZone var timeZone: TimeZone
val sameTimeZone: Boolean // The TimeZone is the same on the phone and on the pod
val lastUpdatedSystem: Long // System.currentTimeMillis() val lastUpdatedSystem: Long // System.currentTimeMillis()
val lastStatusResponseReceived: Long val lastStatusResponseReceived: Long
val time: DateTime? val time: ZonedDateTime?
val timeDrift: Duration? val timeDrift: java.time.Duration?
val expiry: DateTime? val expiry: ZonedDateTime?
val messageSequenceNumber: Short val messageSequenceNumber: Short
val sequenceNumberOfLastProgrammingCommand: Short? val sequenceNumberOfLastProgrammingCommand: Short?
@ -104,6 +101,9 @@ interface OmnipodDashPodStateManager {
- after getPodStatus was successful(we have an up-to-date podStatus) - after getPodStatus was successful(we have an up-to-date podStatus)
*/ */
fun recoverActivationFromPodStatus(): String? 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( data class ActiveCommand(
val sequence: Short, val sequence: Short,

View file

@ -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.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult 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.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.definition.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.DefaultStatusResponse 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 info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single 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.io.Serializable
import java.time.Duration
import java.time.Instant
import java.time.ZonedDateTime
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -107,6 +105,12 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
store() store()
} }
override val sameTimeZone: Boolean
get() {
val now = System.currentTimeMillis()
return TimeZone.getDefault().getOffset(now) == timeZone.getOffset(now)
}
override val bluetoothVersion: SoftwareVersion? override val bluetoothVersion: SoftwareVersion?
get() = podState.bleVersion get() = podState.bleVersion
@ -183,36 +187,40 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
override val lastStatusResponseReceived: Long override val lastStatusResponseReceived: Long
get() = podState.lastStatusResponseReceived get() = podState.lastStatusResponseReceived
override val time: DateTime? override val time: ZonedDateTime?
get() { get() {
val minutesSinceActivation = podState.minutesSinceActivation val minutesSinceActivation = podState.minutesSinceActivation
val activationTime = podState.activationTime val activationTime = podState.activationTime
if ((activationTime != null) && (minutesSinceActivation != null)) { if ((activationTime != null) && (minutesSinceActivation != null)) {
return DateTime(activationTime) return ZonedDateTime.ofInstant(Instant.ofEpochMilli(activationTime), timeZone.toZoneId())
.plusMinutes(minutesSinceActivation.toInt()) .plusMinutes(minutesSinceActivation.toLong())
.plus(Duration(podState.lastUpdatedSystem, System.currentTimeMillis())) .plus(Duration.ofMillis(System.currentTimeMillis() - lastUpdatedSystem))
} }
return null return null
} }
override val timeDrift: Duration? override val timeDrift: Duration?
get() { get() {
return Duration(DateTime.now(), time) return Duration.between(ZonedDateTime.now(), time)
} }
override val expiry: DateTime? override val expiry: ZonedDateTime?
// TODO: Consider storing expiry datetime in pod state saving continuously recalculating to the same value
get() { get() {
val podLifeInHours = podLifeInHours val podLifeInHours = podLifeInHours
val activationTime = podState.activationTime val minutesSinceActivation = podState.minutesSinceActivation
if (podLifeInHours != null && activationTime != null) { if (podLifeInHours != null && minutesSinceActivation != null) {
return DateTime(podState.activationTime).plusHours(podLifeInHours.toInt()) return ZonedDateTime.now()
.plusHours(podLifeInHours.toLong())
.minusMinutes(minutesSinceActivation.toLong())
.plus(Duration.ofMillis(System.currentTimeMillis() - lastUpdatedSystem))
} }
return null return null
} }
override var bluetoothConnectionState: OmnipodDashPodStateManager.BluetoothConnectionState override var bluetoothConnectionState: OmnipodDashPodStateManager.BluetoothConnectionState
@Synchronized
get() = podState.bluetoothConnectionState get() = podState.bluetoothConnectionState
@Synchronized
set(bluetoothConnectionState) { set(bluetoothConnectionState) {
podState.bluetoothConnectionState = bluetoothConnectionState podState.bluetoothConnectionState = bluetoothConnectionState
rxBus.send(EventOmnipodDashPumpValuesChanged()) rxBus.send(EventOmnipodDashPumpValuesChanged())
@ -283,29 +291,29 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
requestedBolus: Double? requestedBolus: Double?
): ):
Single<OmnipodDashPodStateManager.ActiveCommand> { Single<OmnipodDashPodStateManager.ActiveCommand> {
return Single.create { source -> return Single.create { source ->
if (activeCommand == null) { if (activeCommand == null) {
val command = OmnipodDashPodStateManager.ActiveCommand( val command = OmnipodDashPodStateManager.ActiveCommand(
podState.messageSequenceNumber, podState.messageSequenceNumber,
createdRealtime = SystemClock.elapsedRealtime(), createdRealtime = SystemClock.elapsedRealtime(),
historyId = historyId, historyId = historyId,
sendError = null, sendError = null,
basalProgram = basalProgram, basalProgram = basalProgram,
tempBasal = tempBasal, tempBasal = tempBasal,
requestedBolus = requestedBolus 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 @Synchronized
override fun observeNoActiveCommand(): Completable { 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 @Synchronized
override fun getCommandConfirmationFromState(): CommandConfirmationFromState { override fun getCommandConfirmationFromState(): CommandConfirmationFromState {
return podState.activeCommand?.run { return podState.activeCommand?.run {
@ -429,7 +463,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
override fun onStart() { override fun onStart() {
when (getCommandConfirmationFromState()) { when (getCommandConfirmationFromState()) {
CommandConfirmationSuccess, CommandConfirmationDenied -> { CommandConfirmationSuccess, CommandConfirmationDenied -> {
val now = System.currentTimeMillis() val now = SystemClock.elapsedRealtime()
val newCommand = podState.activeCommand?.copy( val newCommand = podState.activeCommand?.copy(
createdRealtime = now, createdRealtime = now,
sentRealtime = now + 1 sentRealtime = now + 1
@ -439,7 +473,7 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
} }
CommandSendingNotConfirmed -> { CommandSendingNotConfirmed -> {
val now = System.currentTimeMillis() val now = SystemClock.elapsedRealtime()
val newCommand = podState.activeCommand?.copy( val newCommand = podState.activeCommand?.copy(
createdRealtime = now, createdRealtime = now,
sentRealtime = now + 1 sentRealtime = now + 1
@ -611,6 +645,11 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
var firstPrimeBolusVolume: Short? = null var firstPrimeBolusVolume: Short? = null
var secondPrimeBolusVolume: 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 pulsesDelivered: Short? = null
var pulsesRemaining: Short? = null var pulsesRemaining: Short? = null
var podStatus: PodStatus? = null var podStatus: PodStatus? = null

View file

@ -44,8 +44,8 @@ import info.nightscout.androidaps.utils.ui.UIRunnable
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.plusAssign
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.joda.time.DateTime import java.time.Duration
import org.joda.time.Duration import java.time.ZonedDateTime
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -269,34 +269,41 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
// Update time on Pod // Update time on Pod
podInfoBinding.timeOnPod.text = podStateManager.time?.let { 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 } ?: PLACEHOLDER
val timeDeviationTooBig = podStateManager.timeDrift?.let {
Duration.ofMinutes(MAX_TIME_DEVIATION_MINUTES).minus(
it.abs()
).isNegative
} ?: false
podInfoBinding.timeOnPod.setTextColor( podInfoBinding.timeOnPod.setTextColor(
podStateManager.timeDrift?.let { when {
if (it.abs().isLongerThan(Duration.standardMinutes(MAX_TIME_DEVIATION_MINUTES))) { !podStateManager.sameTimeZone ->
Color.RED Color.MAGENTA
} else { timeDeviationTooBig ->
Color.YELLOW
else ->
Color.WHITE Color.WHITE
} }
} ?: Color.WHITE
) )
// Update Pod expiry time // Update Pod expiry time
val expiresAt = podStateManager.expiry val expiresAt = podStateManager.expiry
if (expiresAt == null) { podInfoBinding.podExpiryDate.text = expiresAt?.let {
podInfoBinding.podExpiryDate.text = PLACEHOLDER dateUtil.dateAndTimeString(it.toEpochSecond() * 1000)
podInfoBinding.podExpiryDate.setTextColor(Color.WHITE)
} else {
podInfoBinding.podExpiryDate.text = readableZonedTime(expiresAt)
podInfoBinding.podExpiryDate.setTextColor(
if (DateTime.now().isAfter(expiresAt)) {
Color.RED
} else {
Color.WHITE
}
)
} }
?: PLACEHOLDER
podInfoBinding.podExpiryDate.setTextColor(
if (expiresAt != null && ZonedDateTime.now().isAfter(expiresAt))
Color.RED
else
Color.WHITE
)
podStateManager.alarmType?.let { podStateManager.alarmType?.let {
errors.add( errors.add(
@ -371,14 +378,14 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
private fun updateLastConnection() { private fun updateLastConnection() {
if (podStateManager.isUniqueIdSet) { if (podStateManager.isUniqueIdSet) {
podInfoBinding.lastConnection.text = readableDuration( podInfoBinding.lastConnection.text = readableDuration(
Duration( Duration.ofMillis(
podStateManager.lastUpdatedSystem, System.currentTimeMillis() -
System podStateManager.lastUpdatedSystem,
.currentTimeMillis()
) )
) )
val lastConnectionColor = val lastConnectionColor =
if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().millis)) { if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().toMillis())) {
Color.RED Color.RED
} else { } else {
Color.WHITE Color.WHITE
@ -425,12 +432,14 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
} }
} }
val podStatusColor = val podStatusColor = when {
if (!podStateManager.isActivationCompleted || podStateManager.isPodKaput || podStateManager.isSuspended) { !podStateManager.isActivationCompleted || podStateManager.isPodKaput || podStateManager.isSuspended ->
Color.RED Color.RED
} else { podStateManager.activeCommand != null ->
Color.YELLOW
else ->
Color.WHITE Color.WHITE
} }
podInfoBinding.podStatus.setTextColor(podStatusColor) podInfoBinding.podStatus.setTextColor(podStatusColor)
} }
@ -444,7 +453,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
R.string.omnipod_common_overview_last_bolus_value, R.string.omnipod_common_overview_last_bolus_value,
omnipodDashPumpPlugin.model().determineCorrectBolusSize(requestedBolus), omnipodDashPumpPlugin.model().determineCorrectBolusSize(requestedBolus),
resourceHelper.gs(R.string.insulin_unit_shortname), resourceHelper.gs(R.string.insulin_unit_shortname),
readableDuration(Duration(it.createdRealtime, SystemClock.elapsedRealtime())) readableDuration(Duration.ofMillis(SystemClock.elapsedRealtime() - it.createdRealtime))
) )
text += " (uncertain) " text += " (uncertain) "
textColor = Color.RED textColor = Color.RED
@ -464,7 +473,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
R.string.omnipod_common_overview_last_bolus_value, R.string.omnipod_common_overview_last_bolus_value,
omnipodDashPumpPlugin.model().determineCorrectBolusSize(bolusSize), omnipodDashPumpPlugin.model().determineCorrectBolusSize(bolusSize),
resourceHelper.gs(R.string.insulin_unit_shortname), resourceHelper.gs(R.string.insulin_unit_shortname),
readableDuration(Duration(it.startTime, System.currentTimeMillis())) readableDuration(Duration.ofMillis(System.currentTimeMillis() - it.startTime))
) )
if (!it.deliveryComplete) { if (!it.deliveryComplete) {
textColor = Color.YELLOW 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 { private fun readableDuration(duration: Duration): String {
val hours = duration.standardHours.toInt() val hours = duration.toHours().toInt()
val minutes = duration.standardMinutes.toInt() val minutes = duration.toMinutes().toInt()
val seconds = duration.standardSeconds.toInt() val seconds = duration.seconds
when { when {
seconds < 10 -> { seconds < 10 -> {
return resourceHelper.gs(R.string.omnipod_common_moments_ago) 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 // FIXME ideally we should just have access to LocalAlertUtils here
private fun getPumpUnreachableTimeout(): Duration { private fun getPumpUnreachableTimeout(): Duration {
return Duration.standardMinutes( return Duration.ofMinutes(
sp.getInt( sp.getInt(
R.string.key_pump_unreachable_threshold_minutes, R.string.key_pump_unreachable_threshold_minutes,
Constants.DEFAULT_PUMP_UNREACHABLE_THRESHOLD_MINUTES Constants.DEFAULT_PUMP_UNREACHABLE_THRESHOLD_MINUTES

View file

@ -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.R
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager 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.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.Single
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject import javax.inject.Inject
@ -16,9 +18,10 @@ import javax.inject.Inject
class DashInitializePodViewModel @Inject constructor( class DashInitializePodViewModel @Inject constructor(
private val omnipodManager: OmnipodDashManager, private val omnipodManager: OmnipodDashManager,
injector: HasAndroidInjector, injector: HasAndroidInjector,
logger: AAPSLogger logger: AAPSLogger,
private val sp: SP,
private val podStateManager: OmnipodDashPodStateManager,
) : InitializePodViewModel(injector, logger) { ) : InitializePodViewModel(injector, logger) {
override fun isPodInAlarm(): Boolean = false // TODO override fun isPodInAlarm(): Boolean = false // TODO
override fun isPodActivationTimeExceeded(): Boolean = false // TODO override fun isPodActivationTimeExceeded(): Boolean = false // TODO
@ -27,8 +30,14 @@ class DashInitializePodViewModel @Inject constructor(
override fun doExecuteAction(): Single<PumpEnactResult> = override fun doExecuteAction(): Single<PumpEnactResult> =
Single.create { source -> Single.create { source ->
// TODO use configured value for low reservoir trigger val lowReservoirAlertEnabled = sp.getBoolean(R.string.key_omnipod_common_low_reservoir_alert_enabled, true)
val disposable = omnipodManager.activatePodPart1(AlertTrigger.ReservoirVolumeTrigger(200)).subscribeBy( 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 -> onNext = { podEvent ->
logger.debug( logger.debug(
LTag.PUMP, LTag.PUMP,
@ -41,6 +50,7 @@ class DashInitializePodViewModel @Inject constructor(
}, },
onComplete = { onComplete = {
logger.debug("Pod activation part 1 completed") logger.debug("Pod activation part 1 completed")
podStateManager.updateLowReservoirAlertSettings(lowReservoirAlertEnabled, lowReservoirAlertUnits)
source.onSuccess(PumpEnactResult(injector).success(true)) source.onSuccess(PumpEnactResult(injector).success(true))
} }
) )

View file

@ -8,12 +8,16 @@ import info.nightscout.androidaps.interfaces.ProfileFunction
import info.nightscout.androidaps.interfaces.PumpSync import info.nightscout.androidaps.interfaces.PumpSync
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag 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.common.defs.PumpType
import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.activation.viewmodel.action.InsertCannulaViewModel 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.R
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager 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.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram
import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject import javax.inject.Inject
@ -23,6 +27,9 @@ class DashInsertCannulaViewModel @Inject constructor(
private val profileFunction: ProfileFunction, private val profileFunction: ProfileFunction,
private val pumpSync: PumpSync, private val pumpSync: PumpSync,
private val podStateManager: OmnipodDashPodStateManager, private val podStateManager: OmnipodDashPodStateManager,
private val rxBus: RxBusWrapper,
private val sp: SP,
injector: HasAndroidInjector, injector: HasAndroidInjector,
logger: AAPSLogger logger: AAPSLogger
) : InsertCannulaViewModel(injector, logger) { ) : InsertCannulaViewModel(injector, logger) {
@ -45,7 +52,15 @@ class DashInsertCannulaViewModel @Inject constructor(
profile, profile,
basalProgram 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 -> onNext = { podEvent ->
logger.debug( logger.debug(
LTag.PUMP, LTag.PUMP,
@ -58,6 +73,7 @@ class DashInsertCannulaViewModel @Inject constructor(
}, },
onComplete = { onComplete = {
logger.debug("Pod activation part 2 completed") logger.debug("Pod activation part 2 completed")
podStateManager.basalProgram = basalProgram
pumpSync.connectNewPump() pumpSync.connectNewPump()
pumpSync.insertTherapyEventIfNewWithTimestamp( pumpSync.insertTherapyEventIfNewWithTimestamp(
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
@ -71,6 +87,14 @@ class DashInsertCannulaViewModel @Inject constructor(
pumpType = PumpType.OMNIPOD_DASH, pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = podStateManager.uniqueId?.toString() ?: "n/a" 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)) source.onSuccess(PumpEnactResult(injector).success(true))
} }
) )

View file

@ -65,11 +65,33 @@
validate:maxNumber="50" validate:maxNumber="50"
validate:minNumber="5" validate:minNumber="5"
validate:testType="numericRange" /> validate:testType="numericRange" />
<!--
<SwitchPreference
android:defaultValue="false"
android:key="@string/key_omnipod_common_automatically_silence_alerts_enabled"
android:title="@string/omnipod_common_preferences_automatically_silence_alerts" />
-->
</PreferenceCategory>
<PreferenceCategory
android:key="@string/key_omnipod_common_preferences_category_notifications_settings"
android:title="@string/omnipod_common_preferences_category_notifications"
app:initialExpandedChildrenCount="0">
<SwitchPreference <SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="@string/key_omnipod_common_automatically_silence_alerts_enabled" android:key="@string/key_omnipod_common_notification_uncertain_tbr_sound_enabled"
android:title="@string/omnipod_common_preferences_automatically_silence_alerts" /> android:title="@string/omnipod_common_preferences_notification_uncertain_tbr_sound_enabled" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/key_omnipod_common_notification_uncertain_smb_sound_enabled"
android:title="@string/omnipod_common_preferences_notification_uncertain_smb_sound_enabled" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/key_omnipod_common_notification_uncertain_bolus_sound_enabled"
android:title="@string/omnipod_common_preferences_notification_uncertain_bolus_sound_enabled" />
</PreferenceCategory> </PreferenceCategory>