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 {
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 {

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_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_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 -->
<string name="omnipod_common_pod_management_title">Pod Management</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_alerts">Alerts</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 -->
<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.data.DetailedBolusInfo
import info.nightscout.androidaps.data.PumpEnactResult
import info.nightscout.androidaps.events.EventPreferenceChange
import info.nightscout.androidaps.events.EventProfileSwitchChanged
import info.nightscout.androidaps.events.EventRefreshOverview
import info.nightscout.androidaps.events.EventTempBasalChange
import info.nightscout.androidaps.interfaces.*
import info.nightscout.androidaps.logging.AAPSLogger
@ -25,10 +27,7 @@ import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BeepType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.DeliveryStatus
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.PodConstants
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.ResponseType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.CommandConfirmed
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
@ -40,16 +39,24 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.OmnipodDashOvervi
import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram
import info.nightscout.androidaps.queue.commands.Command
import info.nightscout.androidaps.queue.commands.CustomCommand
import info.nightscout.androidaps.utils.FabricPrivacy
import info.nightscout.androidaps.utils.T
import info.nightscout.androidaps.utils.TimeChangeType
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.rx.AapsSchedulers
import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Completable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import org.json.JSONObject
import java.time.Duration
import java.time.ZonedDateTime
import java.util.*
import java.util.concurrent.CountDownLatch
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.concurrent.thread
import kotlin.math.ceil
import kotlin.random.Random
@ -63,6 +70,8 @@ class OmnipodDashPumpPlugin @Inject constructor(
private val pumpSync: PumpSync,
private val rxBus: RxBusWrapper,
private val context: Context,
private val aapsSchedulers: AapsSchedulers,
private val fabricPrivacy: FabricPrivacy,
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
@ -70,9 +79,13 @@ class OmnipodDashPumpPlugin @Inject constructor(
commandQueue: CommandQueueProvider
) : PumpPluginBase(pluginDescription, injector, aapsLogger, resourceHelper, commandQueue), Pump {
@Volatile var bolusCanceled = false
@Volatile var bolusDeliveryInProgress = false
private val handler: Handler = Handler(Looper.getMainLooper())
lateinit private var statusChecker: Runnable
var nextPodWarningCheck : Long = 0
private lateinit var statusChecker: Runnable
var nextPodWarningCheck: Long = 0
@Volatile var stopConnecting: CountDownLatch? = null
private var disposables: CompositeDisposable = CompositeDisposable()
companion object {
private const val BOLUS_RETRY_INTERVAL_MS = 2000.toLong()
@ -95,10 +108,32 @@ class OmnipodDashPumpPlugin @Inject constructor(
statusChecker = Runnable {
refreshStatusOnUnacknowledgedCommands()
updatePodWarnings()
// createFakeTBRWhenNoActivePod()
// TODO: this is called from the main thread
handler.postDelayed(statusChecker, STATUS_CHECK_INTERVAL_MS)
}
}
private fun createFakeTBRWhenNoActivePod() {
if (!podStateManager.isPodRunning) {
val expectedState = pumpSync.expectedPumpState()
val tbr = expectedState.temporaryBasal
if (tbr == null || tbr.rate != 0.0) {
aapsLogger.info(LTag.PUMP, "createFakeTBRWhenNoActivePod")
pumpSync.syncTemporaryBasalWithPumpId(
timestamp = System.currentTimeMillis(),
rate = 0.0,
duration = T.mins(PodConstants.MAX_POD_LIFETIME.toMinutes()).msecs(),
isAbsolute = true,
type = PumpSync.TemporaryBasalType.PUMP_SUSPEND,
pumpId = Random.Default.nextLong(), // we don't use this, just make sure it's unique
pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = serialNumber()
)
}
}
}
private fun updatePodWarnings() {
if (System.currentTimeMillis() > nextPodWarningCheck) {
if (!podStateManager.isPodRunning) {
@ -121,7 +156,17 @@ class OmnipodDashPumpPlugin @Inject constructor(
rxBus.send(EventNewNotification(notification))
} else {
rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED))
// TODO: time out of sync notification?
if (!podStateManager.sameTimeZone) {
val notification =
Notification(
Notification.OMNIPOD_TIME_OUT_OF_SYNC,
"Timezone on pod is different from the timezone on phone. " +
"Basal rate is incorrect" +
"Switch profile to fix",
Notification.NORMAL
)
rxBus.send(EventNewNotification(notification))
}
}
}
nextPodWarningCheck = DateTimeUtil.getTimeInFutureFromMinutes(15)
@ -132,13 +177,13 @@ class OmnipodDashPumpPlugin @Inject constructor(
if (podStateManager.isPodRunning &&
podStateManager.activeCommand != null &&
commandQueue.size() == 0 &&
commandQueue.performing() == null) {
commandQueue.performing() == null
) {
commandQueue.readStatus("Unconfirmed command", null)
}
}
override fun isInitialized(): Boolean {
// TODO
return true
}
@ -153,37 +198,64 @@ class OmnipodDashPumpPlugin @Inject constructor(
override fun isConnected(): Boolean {
return true
return !podStateManager.isPodRunning ||
podStateManager.bluetoothConnectionState == OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED
}
override fun isConnecting(): Boolean {
// TODO
return false
return stopConnecting != null
}
override fun isHandshakeInProgress(): Boolean {
// TODO
return false
return stopConnecting != null &&
podStateManager.bluetoothConnectionState == OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED
}
override fun finishHandshaking() {
// TODO
}
override fun connect(reason: String) {
// empty on purpose
aapsLogger.info(LTag.PUMP, "connect reason=$reason")
podStateManager.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING
synchronized(this) {
stopConnecting?.let {
aapsLogger.warn(LTag.PUMP, "Already connecting: $stopConnecting")
return
}
val stop = CountDownLatch(1)
stopConnecting = stop
}
thread(
start = true,
name = "ConnectionThread",
) {
try {
stopConnecting?.let {
val error = omnipodManager.connect(it).ignoreElements().blockingGet()
aapsLogger.info(LTag.PUMPCOMM, "connect error=$error")
}
} finally {
synchronized(this) {
stopConnecting = null
}
}
}
}
override fun disconnect(reason: String) {
// TODO
aapsLogger.info(LTag.PUMP, "disconnect reason=$reason")
stopConnecting?.let { it.countDown() }
omnipodManager.disconnect(false)
}
override fun stopConnecting() {
// TODO
aapsLogger.info(LTag.PUMP, "stopConnecting")
stopConnecting?.let { it.countDown() }
omnipodManager.disconnect(true)
}
override fun getPumpStatus(reason: String) {
aapsLogger.debug(LTag.PUMP, "getPumpStatus reason=$reason")
if (reason != "REQUESTED BY USER" && !podStateManager.isActivationCompleted) {
@ -226,7 +298,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
pumpSync.syncTemporaryBasalWithPumpId(
timestamp = System.currentTimeMillis(),
rate = 0.0,
duration = T.mins(PodConstants.MAX_POD_LIFETIME.standardMinutes).msecs(),
duration = T.mins(PodConstants.MAX_POD_LIFETIME.toMinutes()).msecs(),
isAbsolute = true,
type = PumpSync.TemporaryBasalType.PUMP_SUSPEND,
pumpId = Random.Default.nextLong(), // we don't use this, just make sure it's unique
@ -279,8 +351,8 @@ class OmnipodDashPumpPlugin @Inject constructor(
podStateManager.createActiveCommand(historyId, basalProgram = basalProgram)
},
command = omnipodManager.setBasalProgram(basalProgram, hasBasalBeepEnabled()).ignoreElements(),
post = failWhenUnconfirmed(deliverySuspended), // mark as failed even if it worked OK and try again vs. mark ok and
// deny later
post = failWhenUnconfirmed(deliverySuspended),
// mark as failed even if it worked OK and try again vs. mark ok and deny later
).toPumpEnactResult()
}
@ -305,6 +377,8 @@ class OmnipodDashPumpPlugin @Inject constructor(
}
Completable.error(java.lang.IllegalStateException("Command not confirmed"))
} else {
showNotification(Notification.PROFILE_SET_OK, "Profile set OK", Notification.INFO, null)
Completable.complete()
}
}
@ -320,7 +394,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
.map {
pumpSyncTempBasal(
0.0,
PodConstants.MAX_POD_LIFETIME.standardMinutes,
PodConstants.MAX_POD_LIFETIME.toMinutes(),
PumpSync.TemporaryBasalType.PUMP_SUSPEND
)
rxBus.send(EventTempBasalChange())
@ -340,10 +414,38 @@ class OmnipodDashPumpPlugin @Inject constructor(
super.onStart()
podStateManager.onStart()
handler.postDelayed(statusChecker, STATUS_CHECK_INTERVAL_MS)
disposables += rxBus
.toObservable(EventPreferenceChange::class.java)
.observeOn(aapsSchedulers.main)
.subscribe(
{
if (it.isChanged(
resourceHelper,
R.string.key_omnipod_common_expiration_reminder_enabled
) ||
it.isChanged(
resourceHelper,
R.string.key_omnipod_common_expiration_reminder_hours_before_shutdown
) ||
it.isChanged(
resourceHelper,
R.string.key_omnipod_common_low_reservoir_alert_enabled
) ||
it.isChanged(
resourceHelper,
R.string.key_omnipod_common_low_reservoir_alert_units
)
) {
commandQueue.customCommand(CommandUpdateAlertConfiguration(), null)
}
},
fabricPrivacy::logException
)
}
override fun onStop() {
super.onStop()
disposables.clear()
handler.removeCallbacks(statusChecker)
}
@ -363,7 +465,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
// TODO: what do we have to answer here if delivery is suspended?
val running = podStateManager.basalProgram
val equal = (mapProfileToBasalProgram(profile) == running)
aapsLogger.info(LTag.PUMP, "isThisProfileSet: $equal")
aapsLogger.info(LTag.PUMP, "set: $equal. profile=$profile, running=$running")
return equal
}
@ -400,7 +502,8 @@ class OmnipodDashPumpPlugin @Inject constructor(
override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult {
try {
aapsLogger.info(LTag.PUMP, "Delivering treatment: $detailedBolusInfo")
bolusDeliveryInProgress = true
aapsLogger.info(LTag.PUMP, "Delivering treatment: $detailedBolusInfo $bolusCanceled")
val beepsConfigurationKey = if (detailedBolusInfo.bolusType == DetailedBolusInfo.BolusType.SMB)
R.string.key_omnipod_common_smb_beeps_enabled
else
@ -469,9 +572,19 @@ class OmnipodDashPumpPlugin @Inject constructor(
)
} else {
if (podStateManager.activeCommand != null) {
val sound = if (sp.getBoolean(
R.string
.key_omnipod_common_notification_uncertain_bolus_sound_enabled,
true
)
)
R.raw.boluserror
else
0
showErrorDialog(
"Bolus delivery status uncertain. Refresh pod status to confirm or deny.",
R.raw.boluserror
sound
)
}
}
@ -487,6 +600,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
return ret
} finally {
bolusCanceled = false
bolusDeliveryInProgress = false
}
}
@ -621,7 +735,9 @@ class OmnipodDashPumpPlugin @Inject constructor(
override fun stopBolusDelivering() {
aapsLogger.info(LTag.PUMP, "stopBolusDelivering called")
bolusCanceled = true
if (bolusDeliveryInProgress) {
bolusCanceled = true
}
}
override fun setTempBasalAbsolute(
@ -910,7 +1026,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
.map {
pumpSyncTempBasal(
0.0,
PodConstants.MAX_POD_LIFETIME.standardMinutes,
PodConstants.MAX_POD_LIFETIME.toMinutes(),
PumpSync.TemporaryBasalType.PUMP_SUSPEND
)
}
@ -969,8 +1085,67 @@ class OmnipodDashPumpPlugin @Inject constructor(
}
private fun updateAlertConfiguration(): PumpEnactResult {
// TODO
return PumpEnactResult(injector).success(false).enacted(false).comment("NOT IMPLEMENTED")
val expirationReminderEnabled = sp.getBoolean(R.string.key_omnipod_common_expiration_reminder_enabled, true)
val expirationHours = sp.getInt(R.string.key_omnipod_common_expiration_reminder_hours_before_shutdown, 7)
val lowReservoirAlertEnabled = sp.getBoolean(R.string.key_omnipod_common_low_reservoir_alert_enabled, true)
val lowReservoirAlertUnits = sp.getInt(R.string.key_omnipod_common_low_reservoir_alert_units, 10)
if (!podStateManager.differentAlertSettings(
expirationReminderEnabled,
expirationHours,
lowReservoirAlertEnabled,
lowReservoirAlertUnits
)
) {
return PumpEnactResult(injector).success(true).enacted(false)
}
val podLifeLeft = Duration.between(ZonedDateTime.now(), podStateManager.expiry)
val expiryAlertDelay = podLifeLeft.minus(Duration.ofHours(expirationHours.toLong()))
if (expiryAlertDelay.isNegative) {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"updateAlertConfiguration negative " +
"expiryAlertDuration=$expiryAlertDelay"
)
PumpEnactResult(injector).success(false).enacted(false)
}
val alerts = listOf(
AlertConfiguration(
AlertType.LOW_RESERVOIR,
enabled = lowReservoirAlertEnabled,
durationInMinutes = 0,
autoOff = false,
AlertTrigger.ReservoirVolumeTrigger((lowReservoirAlertUnits * 10).toShort()),
BeepType.FOUR_TIMES_BIP_BEEP,
BeepRepetitionType.XXX
),
AlertConfiguration(
AlertType.USER_SET_EXPIRATION,
enabled = expirationReminderEnabled,
durationInMinutes = 0,
autoOff = false,
AlertTrigger.TimerTrigger(
expiryAlertDelay.toMinutes().toShort()
),
BeepType.FOUR_TIMES_BIP_BEEP,
BeepRepetitionType.XXX2
)
)
return executeProgrammingCommand(
historyEntry = history.createRecord(OmnipodCommandType.CONFIGURE_ALERTS),
command = omnipodManager.programAlerts(alerts).ignoreElements(),
post = podStateManager.updateExpirationAlertSettings(
expirationReminderEnabled,
expirationHours
).andThen(
podStateManager.updateExpirationAlertSettings(
lowReservoirAlertEnabled,
lowReservoirAlertUnits
)
)
).toPumpEnactResult()
}
private fun playTestBeep(): PumpEnactResult {
@ -1029,11 +1204,17 @@ class OmnipodDashPumpPlugin @Inject constructor(
.map { handleCommandConfirmation(it) }
.ignoreElement(),
checkPodKaput(),
refreshOverview(),
post,
)
)
}
private fun refreshOverview(): Completable = Completable.defer {
rxBus.send(EventRefreshOverview("Dash command", false))
Completable.complete()
}
private fun handleCommandConfirmation(confirmation: CommandConfirmed) {
val command = confirmation.command
val historyEntry = history.getById(command.historyId)
@ -1065,6 +1246,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED))
}
rxBus.send(EventDismissNotification(Notification.OMNIPOD_TBR_ALERTS))
rxBus.send(EventDismissNotification(Notification.OMNIPOD_TIME_OUT_OF_SYNC))
}
OmnipodCommandType.SET_BASAL_PROFILE -> {
@ -1086,6 +1268,7 @@ class OmnipodDashPumpPlugin @Inject constructor(
rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_SUSPENDED))
rxBus.send(EventDismissNotification(Notification.FAILED_UPDATE_PROFILE))
rxBus.send(EventDismissNotification(Notification.OMNIPOD_TBR_ALERTS))
rxBus.send(EventDismissNotification(Notification.OMNIPOD_TIME_OUT_OF_SYNC))
}
}
@ -1186,14 +1369,22 @@ class OmnipodDashPumpPlugin @Inject constructor(
message,
urgency
)
// TODO add back sound when we have options to disable it
/*
if (sound != null) {
if (sound != null && soundEnabledForNotificationType(id)) {
notification.soundId = sound
}*/
}
rxBus.send(EventNewNotification(notification))
}
private fun soundEnabledForNotificationType(notificationType: Int): Boolean {
return when (notificationType) {
Notification.OMNIPOD_TBR_ALERTS ->
sp.getBoolean(R.string.key_omnipod_common_notification_uncertain_tbr_sound_enabled, true)
Notification.OMNIPOD_UNCERTAIN_SMB ->
sp.getBoolean(R.string.key_omnipod_common_notification_uncertain_smb_sound_enabled, true)
else -> true
}
}
private fun dismissNotification(id: Int) {
rxBus.send(EventDismissNotification(id))
}

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 io.reactivex.Observable
import java.util.*
import java.util.concurrent.CountDownLatch
interface OmnipodDashManager {
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>
@ -39,4 +40,8 @@ interface OmnipodDashManager {
fun silenceAlerts(alertTypes: EnumSet<AlertType>): 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.GetVersionCommand.Companion.DEFAULT_UNIQUE_ID
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.PodConstants.Companion.MAX_POD_LIFETIME
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.utils.rx.AapsSchedulers
import io.reactivex.Observable
import io.reactivex.functions.Action
import io.reactivex.functions.Consumer
import java.time.Duration
import java.time.ZonedDateTime
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@ -74,6 +78,22 @@ class OmnipodDashManagerImpl @Inject constructor(
}
}
override fun disconnect(closeGatt: Boolean) {
bleManager.disconnect(closeGatt)
}
override fun connect(stop: CountDownLatch): Observable<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>
get() = Observable.defer {
bleManager.connect()
@ -221,10 +241,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observeConnectToPod,
observeActivationPart1Commands(lowReservoirAlertTrigger)
).doOnComplete(ActivationProgressUpdater(ActivationProgress.PHASE_1_COMPLETED))
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
.interceptPodEvents()
}
private fun observeActivationPart1Commands(lowReservoirAlertTrigger: AlertTrigger.ReservoirVolumeTrigger?): Observable<PodEvent> {
@ -320,20 +337,19 @@ class OmnipodDashManagerImpl @Inject constructor(
return observables.reversed()
}
override fun activatePodPart2(basalProgram: BasalProgram): Observable<PodEvent> {
override fun activatePodPart2(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?):
Observable<PodEvent> {
return Observable.concat(
observePodReadyForActivationPart2,
observeConnectToPod,
observeActivationPart2Commands(basalProgram)
observeActivationPart2Commands(basalProgram, userConfiguredExpirationHours)
).doOnComplete(ActivationProgressUpdater(ActivationProgress.COMPLETED))
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
.interceptPodEvents()
}
private fun observeActivationPart2Commands(basalProgram: BasalProgram): Observable<PodEvent> {
val observables = createActivationPart2Observables(basalProgram)
private fun observeActivationPart2Commands(basalProgram: BasalProgram, userConfiguredExpirationHours: Long?):
Observable<PodEvent> {
val observables = createActivationPart2Observables(basalProgram, userConfiguredExpirationHours)
return if (observables.isEmpty()) {
Observable.empty()
@ -342,7 +358,11 @@ class OmnipodDashManagerImpl @Inject constructor(
}
}
private fun createActivationPart2Observables(basalProgram: BasalProgram): List<Observable<PodEvent>> {
private fun createActivationPart2Observables(
basalProgram: BasalProgram,
userConfiguredExpirationHours: Long?
):
List<Observable<PodEvent>> {
val observables = ArrayList<Observable<PodEvent>>()
if (podStateManager.activationProgress.isBefore(ActivationProgress.CANNULA_INSERTED)) {
@ -368,33 +388,60 @@ class OmnipodDashManagerImpl @Inject constructor(
)
}
if (podStateManager.activationProgress.isBefore(ActivationProgress.UPDATED_EXPIRATION_ALERTS)) {
val podLifeLeft = Duration.between(ZonedDateTime.now(), podStateManager.expiry)
val alerts = mutableListOf(
AlertConfiguration(
AlertType.EXPIRATION,
enabled = true,
durationInMinutes = TimeUnit.HOURS.toMinutes(7).toShort(),
autoOff = false,
AlertTrigger.TimerTrigger(
TimeUnit.HOURS.toMinutes(72).toShort()
), // FIXME use activation time
BeepType.FOUR_TIMES_BIP_BEEP,
BeepRepetitionType.XXX3
),
AlertConfiguration(
AlertType.EXPIRATION_IMMINENT,
enabled = true,
durationInMinutes = 0,
autoOff = false,
AlertTrigger.TimerTrigger(
TimeUnit.HOURS.toMinutes(79).toShort()
), // FIXME use activation time
BeepType.FOUR_TIMES_BIP_BEEP,
BeepRepetitionType.XXX4
)
)
val userExpiryAlertDelay = podLifeLeft.minus(
Duration.ofHours(userConfiguredExpirationHours ?: MAX_POD_LIFETIME.toHours() + 1)
)
if (userExpiryAlertDelay.isNegative) {
logger.warn(
LTag.PUMPBTCOMM,
"createActivationPart2Observables negative " +
"expiryAlertDuration=$userExpiryAlertDelay"
)
} else {
alerts.add(
AlertConfiguration(
AlertType.USER_SET_EXPIRATION,
enabled = true,
durationInMinutes = 0,
autoOff = false,
AlertTrigger.TimerTrigger(
userExpiryAlertDelay.toMinutes().toShort()
),
BeepType.FOUR_TIMES_BIP_BEEP,
BeepRepetitionType.XXX2
)
)
}
observables.add(
observeSendProgramAlertsCommand(
listOf(
// FIXME use user configured expiration alert
AlertConfiguration(
AlertType.EXPIRATION,
enabled = true,
durationInMinutes = TimeUnit.HOURS.toMinutes(7).toShort(),
autoOff = false,
AlertTrigger.TimerTrigger(
TimeUnit.HOURS.toMinutes(73).toShort()
), // FIXME use activation time
BeepType.FOUR_TIMES_BIP_BEEP,
BeepRepetitionType.XXX3
),
AlertConfiguration(
AlertType.EXPIRATION_IMMINENT,
enabled = true,
durationInMinutes = TimeUnit.HOURS.toMinutes(1).toShort(),
autoOff = false,
AlertTrigger.TimerTrigger(
TimeUnit.HOURS.toMinutes(79).toShort()
), // FIXME use activation time
BeepType.FOUR_TIMES_BIP_BEEP,
BeepRepetitionType.XXX4
)
),
alerts,
multiCommandFlag = true
).doOnComplete(ActivationProgressUpdater(ActivationProgress.UPDATED_EXPIRATION_ALERTS))
)
@ -414,11 +461,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observeUniqueIdSet,
observeConnectToPod,
observeSendGetPodStatusCommand(type)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
override fun setBasalProgram(basalProgram: BasalProgram, hasBasalBeepEnabled: Boolean): Observable<PodEvent> {
@ -426,11 +469,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendProgramBasalCommand(basalProgram, hasBasalBeepEnabled)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
private fun observeSendStopDeliveryCommand(
@ -461,11 +500,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.ALL, hasBasalBeepEnabled)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
override fun setTime(): Observable<PodEvent> {
@ -495,11 +530,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendProgramTempBasalCommand(rate, durationInMinutes, tempBasalBeeps)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
override fun stopTempBasal(hasTempBasalBeepEnabled: Boolean): Observable<PodEvent> {
@ -507,11 +538,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.TEMP_BASAL, hasTempBasalBeepEnabled)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
override fun bolus(units: Double, confirmationBeeps: Boolean, completionBeeps: Boolean): Observable<PodEvent> {
@ -524,11 +551,7 @@ class OmnipodDashManagerImpl @Inject constructor(
confirmationBeeps,
completionBeeps
)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
override fun stopBolus(beep: Boolean): Observable<PodEvent> {
@ -536,11 +559,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendStopDeliveryCommand(StopDeliveryCommand.DeliveryType.BOLUS, beep)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
private fun observeSendConfigureBeepsCommand(
@ -569,11 +588,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendConfigureBeepsCommand(immediateBeepType = beepType)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
override fun programAlerts(alertConfigurations: List<AlertConfiguration>): Observable<PodEvent> {
@ -581,11 +596,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendProgramAlertsCommand(alertConfigurations)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
private fun observeSendSilenceAlertsCommand(alertTypes: EnumSet<AlertType>): Observable<PodEvent> {
@ -607,11 +618,7 @@ class OmnipodDashManagerImpl @Inject constructor(
observePodRunning,
observeConnectToPod,
observeSendSilenceAlertsCommand(alertTypes)
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
}
private val observeSendDeactivateCommand: Observable<PodEvent>
@ -630,11 +637,7 @@ class OmnipodDashManagerImpl @Inject constructor(
return Observable.concat(
observeConnectToPod,
observeSendDeactivateCommand
)
// TODO these would be common for any observable returned in a public function in this class
.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
).interceptPodEvents()
//
.doOnComplete(podStateManager::reset)
}
@ -711,6 +714,12 @@ class OmnipodDashManagerImpl @Inject constructor(
}
}
private fun Observable<PodEvent>.interceptPodEvents(): Observable<PodEvent> {
return this.doOnNext(PodEventInterceptor())
.doOnError(ErrorInterceptor())
.subscribeOn(aapsSchedulers.io)
}
inner class ErrorInterceptor : Consumer<Throwable> {
override fun accept(throwable: Throwable) {

View file

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

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

View file

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

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.WriteConfirmationError
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.WriteConfirmationSuccess
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandRTS
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.*
import java.util.concurrent.BlockingQueue
import java.util.concurrent.TimeUnit
@ -84,12 +85,17 @@ open class BleIO(
* Called before sending a new message.
* The incoming queues should be empty, so we log when they are not.
*/
fun flushIncomingQueue() {
open fun flushIncomingQueue(): Boolean {
var foundRTS = false
do {
val found = incomingPackets.poll()?.also {
aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: queue not empty, flushing: {${it.toHex()}")
aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: queue not empty, flushing: ${it.toHex()}")
if (it.isNotEmpty() && it[0] == BleCommandRTS.data[0]) {
foundRTS = true
}
}
} while (found != null)
return foundRTS
}
/**

View file

@ -35,7 +35,12 @@ class MessageIO(
@Suppress("ReturnCount")
fun sendMessage(msg: MessagePacket): MessageSendResult {
cmdBleIO.flushIncomingQueue()
val foundRTS = cmdBleIO.flushIncomingQueue()
if (foundRTS) {
val msg = receiveMessage(false)
aapsLogger.warn(LTag.PUMPBTCOMM, "sendMessage received message=$msg")
throw IllegalStateException("Received message while trying to send")
}
dataBleIO.flushIncomingQueue()
val rtsSendResult = cmdBleIO.sendAndConfirmPacket(BleCommandRTS.data)
@ -85,11 +90,13 @@ class MessageIO(
}
@Suppress("ReturnCount")
fun receiveMessage(): MessagePacket? {
val expectRTS = cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS)
if (expectRTS !is BleConfirmSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading RTS: $expectRTS")
return null
fun receiveMessage(readRTS: Boolean = true): MessagePacket? {
if (readRTS) {
val expectRTS = cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS)
if (expectRTS !is BleConfirmSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading RTS: $expectRTS")
return null
}
}
val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandCTS.data)
@ -219,6 +226,6 @@ class MessageIO(
companion object {
private const val MAX_PACKET_READ_TRIES = 4
private const val MESSAGE_READ_TIMEOUT_MS = 2500.toLong()
private const val MESSAGE_READ_TIMEOUT_MS = 5000.toLong()
}
}

View file

@ -5,6 +5,7 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.os.SystemClock
import info.nightscout.androidaps.extensions.toHex
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
@ -13,105 +14,100 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Ids
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ServiceDiscoverer
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ConnectException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.FailedToConnectException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleSendSuccess
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CmdBleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.DataBleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.IncomingPackets
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import java.lang.IllegalArgumentException
import java.util.concurrent.CountDownLatch
sealed class ConnectionState
object Connecting : ConnectionState()
object Connected : ConnectionState()
object NotConnected : ConnectionState()
data class ConnectionWaitCondition(var timeoutMs: Long? = null, val stopConnection: CountDownLatch? = null) {
init {
if (timeoutMs == null && stopConnection == null) {
throw IllegalArgumentException("One of timeoutMs or stopConnection has to be non null")
}
if (timeoutMs != null && stopConnection != null) {
throw IllegalArgumentException("One of timeoutMs or stopConnection has to be null")
}
}
}
class Connection(
private val podDevice: BluetoothDevice,
private val aapsLogger: AAPSLogger,
context: Context,
private val context: Context,
private val podState: OmnipodDashPodStateManager
) : DisconnectHandler {
private val incomingPackets = IncomingPackets()
private val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets, this)
private val gattConnection: BluetoothGatt
private var gattConnection: BluetoothGatt? = null
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
// The session is Synchronized because we can lose the connection right when establishing it
@Volatile
var session: Session? = null
@Synchronized get
@Synchronized set
private val cmdBleIO: CmdBleIO
private val dataBleIO: DataBleIO
init {
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to ${podDevice.address}")
@Volatile
var msgIO: MessageIO? = null
fun connect(connectionWaitCond: ConnectionWaitCondition) {
aapsLogger.debug("Connecting connectionWaitCond=$connectionWaitCond")
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING
val autoConnect = false
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING
gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
// OnDisconnect can be called after this point!!!
val state = waitForConnection(2)
if (state !is Connected) {
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED
throw FailedToConnectException(podDevice.address)
}
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED
val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks)
val discoveredCharacteristics = discoverer.discoverServices()
cmdBleIO = CmdBleIO(
aapsLogger,
discoveredCharacteristics[CharacteristicType.CMD]!!,
incomingPackets
.cmdQueue,
gattConnection,
bleCommCallbacks
)
dataBleIO = DataBleIO(
aapsLogger,
discoveredCharacteristics[CharacteristicType.DATA]!!,
incomingPackets
.dataQueue,
gattConnection,
bleCommCallbacks
)
val sendResult = cmdBleIO.hello()
if (sendResult !is BleSendSuccess) {
throw FailedToConnectException("Could not send HELLO command to ${podDevice.address}")
}
cmdBleIO.readyToRead()
dataBleIO.readyToRead()
}
val msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO)
fun connect(timeoutMultiplier: Int) {
if (session != null) {
disconnect()
}
aapsLogger.debug("Connecting")
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTING
if (!gattConnection.connect()) {
val gatt = gattConnection
?: podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
gattConnection = gatt
if (!gatt.connect()) {
throw FailedToConnectException("connect() returned false")
}
if (waitForConnection(timeoutMultiplier) !is Connected) {
val before = SystemClock.elapsedRealtime()
if (waitForConnection(connectionWaitCond) !is Connected) {
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED
throw FailedToConnectException(podDevice.address)
}
val waitedMs = SystemClock.elapsedRealtime() - before
val timeoutMs = connectionWaitCond.timeoutMs
if (timeoutMs != null) {
var newTimeout = timeoutMs - waitedMs
if (newTimeout < MIN_DISCOVERY_TIMEOUT_MS) {
newTimeout = MIN_DISCOVERY_TIMEOUT_MS
}
connectionWaitCond.timeoutMs = newTimeout
}
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.CONNECTED
val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks)
val discovered = discoverer.discoverServices()
dataBleIO.characteristic = discovered[CharacteristicType.DATA]!!
cmdBleIO.characteristic = discovered[CharacteristicType.CMD]!!
val discoverer = ServiceDiscoverer(aapsLogger, gatt, bleCommCallbacks, this)
val discovered = discoverer.discoverServices(connectionWaitCond)
val cmdBleIO = CmdBleIO(
aapsLogger,
discovered[CharacteristicType.CMD]!!,
incomingPackets
.cmdQueue,
gatt,
bleCommCallbacks
)
val dataBleIO = DataBleIO(
aapsLogger,
discovered[CharacteristicType.DATA]!!,
incomingPackets
.dataQueue,
gatt,
bleCommCallbacks
)
msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO)
// val ret = gattConnection.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// aapsLogger.info(LTag.PUMPBTCOMM, "requestConnectionPriority: $ret")
cmdBleIO.hello()
@ -119,18 +115,33 @@ class Connection(
dataBleIO.readyToRead()
}
fun disconnect() {
aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnecting")
fun disconnect(closeGatt: Boolean) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnecting closeGatt=$closeGatt")
podState.bluetoothConnectionState = OmnipodDashPodStateManager.BluetoothConnectionState.DISCONNECTED
gattConnection.disconnect()
if (closeGatt) {
gattConnection?.close()
gattConnection = null
} else {
gattConnection?.disconnect()
}
bleCommCallbacks.resetConnection()
session = null
msgIO = null
}
private fun waitForConnection(timeoutMultiplier: Int): ConnectionState {
private fun waitForConnection(connectionWaitCond: ConnectionWaitCondition): ConnectionState {
aapsLogger.debug(LTag.PUMPBTCOMM, "waitForConnection connectionWaitCond=$connectionWaitCond")
try {
bleCommCallbacks.waitForConnection(BASE_CONNECT_TIMEOUT_MS * timeoutMultiplier)
connectionWaitCond.timeoutMs?.let {
bleCommCallbacks.waitForConnection(it)
}
connectionWaitCond.stopConnection?.let {
while (!bleCommCallbacks.waitForConnection(STOP_CONNECTING_CHECK_INTERVAL_MS)) {
if (it.count == 0L) {
throw ConnectException("stopConnecting called")
}
}
}
} catch (e: InterruptedException) {
// We are still going to check if connection was successful
aapsLogger.info(LTag.PUMPBTCOMM, "Interrupted while waiting for connection")
@ -148,7 +159,9 @@ class Connection(
}
fun establishSession(ltk: ByteArray, msgSeq: Byte, ids: Ids, eapSqn: ByteArray): EapSqn? {
val eapAkaExchanger = SessionEstablisher(aapsLogger, msgIO, ltk, eapSqn, ids, msgSeq)
val mIO = msgIO ?: throw ConnectException("Connection lost")
val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn, ids, msgSeq)
return when (val keys = eapAkaExchanger.negotiateSessionKeys()) {
is SessionNegotiationResynchronization -> {
if (BuildConfig.DEBUG) {
@ -168,7 +181,7 @@ class Connection(
keys.nonce,
keys.ck
)
session = Session(aapsLogger, msgIO, ids, sessionKeys = keys, enDecrypt = enDecrypt)
session = Session(aapsLogger, mIO, ids, sessionKeys = keys, enDecrypt = enDecrypt)
null
}
}
@ -177,10 +190,12 @@ class Connection(
// This will be called from a different thread !!!
override fun onConnectionLost(status: Int) {
aapsLogger.info(LTag.PUMPBTCOMM, "Lost connection with status: $status")
disconnect()
disconnect(false)
}
companion object {
private const val BASE_CONNECT_TIMEOUT_MS = 10000
const val BASE_CONNECT_TIMEOUT_MS = 10000L
const val MIN_DISCOVERY_TIMEOUT_MS = 10000L
const val STOP_CONNECTING_CHECK_INTERVAL_MS = 500L
}
}

View file

@ -1,7 +1,6 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import info.nightscout.androidaps.extensions.toHex
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
import java.util.*

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

View file

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

View file

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

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

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

View file

@ -44,8 +44,8 @@ import info.nightscout.androidaps.utils.ui.UIRunnable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import org.apache.commons.lang3.StringUtils
import org.joda.time.DateTime
import org.joda.time.Duration
import java.time.Duration
import java.time.ZonedDateTime
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
@ -269,34 +269,41 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
// Update time on Pod
podInfoBinding.timeOnPod.text = podStateManager.time?.let {
readableZonedTime(it)
resourceHelper.gs(
R.string.omnipod_common_time_with_timezone,
dateUtil.dateAndTimeString(it.toEpochSecond() * 1000),
podStateManager.timeZone.getDisplayName(true, TimeZone.SHORT)
)
} ?: PLACEHOLDER
val timeDeviationTooBig = podStateManager.timeDrift?.let {
Duration.ofMinutes(MAX_TIME_DEVIATION_MINUTES).minus(
it.abs()
).isNegative
} ?: false
podInfoBinding.timeOnPod.setTextColor(
podStateManager.timeDrift?.let {
if (it.abs().isLongerThan(Duration.standardMinutes(MAX_TIME_DEVIATION_MINUTES))) {
Color.RED
} else {
when {
!podStateManager.sameTimeZone ->
Color.MAGENTA
timeDeviationTooBig ->
Color.YELLOW
else ->
Color.WHITE
}
} ?: Color.WHITE
}
)
// Update Pod expiry time
val expiresAt = podStateManager.expiry
if (expiresAt == null) {
podInfoBinding.podExpiryDate.text = PLACEHOLDER
podInfoBinding.podExpiryDate.setTextColor(Color.WHITE)
} else {
podInfoBinding.podExpiryDate.text = readableZonedTime(expiresAt)
podInfoBinding.podExpiryDate.setTextColor(
if (DateTime.now().isAfter(expiresAt)) {
Color.RED
} else {
Color.WHITE
}
)
podInfoBinding.podExpiryDate.text = expiresAt?.let {
dateUtil.dateAndTimeString(it.toEpochSecond() * 1000)
}
?: PLACEHOLDER
podInfoBinding.podExpiryDate.setTextColor(
if (expiresAt != null && ZonedDateTime.now().isAfter(expiresAt))
Color.RED
else
Color.WHITE
)
podStateManager.alarmType?.let {
errors.add(
@ -371,14 +378,14 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
private fun updateLastConnection() {
if (podStateManager.isUniqueIdSet) {
podInfoBinding.lastConnection.text = readableDuration(
Duration(
podStateManager.lastUpdatedSystem,
System
.currentTimeMillis()
Duration.ofMillis(
System.currentTimeMillis() -
podStateManager.lastUpdatedSystem,
)
)
val lastConnectionColor =
if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().millis)) {
if (omnipodDashPumpPlugin.isUnreachableAlertTimeoutExceeded(getPumpUnreachableTimeout().toMillis())) {
Color.RED
} else {
Color.WHITE
@ -425,12 +432,14 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
}
}
val podStatusColor =
if (!podStateManager.isActivationCompleted || podStateManager.isPodKaput || podStateManager.isSuspended) {
val podStatusColor = when {
!podStateManager.isActivationCompleted || podStateManager.isPodKaput || podStateManager.isSuspended ->
Color.RED
} else {
podStateManager.activeCommand != null ->
Color.YELLOW
else ->
Color.WHITE
}
}
podInfoBinding.podStatus.setTextColor(podStatusColor)
}
@ -444,7 +453,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
R.string.omnipod_common_overview_last_bolus_value,
omnipodDashPumpPlugin.model().determineCorrectBolusSize(requestedBolus),
resourceHelper.gs(R.string.insulin_unit_shortname),
readableDuration(Duration(it.createdRealtime, SystemClock.elapsedRealtime()))
readableDuration(Duration.ofMillis(SystemClock.elapsedRealtime() - it.createdRealtime))
)
text += " (uncertain) "
textColor = Color.RED
@ -464,7 +473,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
R.string.omnipod_common_overview_last_bolus_value,
omnipodDashPumpPlugin.model().determineCorrectBolusSize(bolusSize),
resourceHelper.gs(R.string.insulin_unit_shortname),
readableDuration(Duration(it.startTime, System.currentTimeMillis()))
readableDuration(Duration.ofMillis(System.currentTimeMillis() - it.startTime))
)
if (!it.deliveryComplete) {
textColor = Color.YELLOW
@ -600,47 +609,10 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
}
}
// private fun getTimeZone(): DateTimeZone {
// // return getSafe(() -> podState.getTimeZone());
// return podStateManager.timeZone
// }
private fun getTimeZone(): TimeZone {
// Return timezone ID (e.g "Europe/Amsterdam")
return podStateManager.timeZone
}
private fun readableZonedTime(time: DateTime): String {
val timeAsJavaData = time.toLocalDateTime().toDate()
return dateUtil.dateAndTimeString(timeAsJavaData.time)
// // TODO: Handle timeZone ID
// val timeZone = getTimeZone()
// if (timeZone == "") {
// // No timezone defined, use local time (default)
// return dateUtil.dateAndTimeString(timeAsJavaData.time)
// }
// else {
// // Get full timezoned time
// val isDaylightTime = timeZone.inDaylightTime(timeAsJavaData)
// val locale = resources.configuration.locales.get(0)
// val timeZoneDisplayName =
// timeZone.getDisplayName(isDaylightTime, TimeZone.SHORT, locale) + " " + timeZone.getDisplayName(
// isDaylightTime,
// TimeZone.LONG,
// locale
// )
// return resourceHelper.gs(
// R.string.omnipod_common_time_with_timezone,
// dateUtil.dateAndTimeString(timeAsJavaData.time),
// timeZoneDisplayName
// )
// }
}
private fun readableDuration(duration: Duration): String {
val hours = duration.standardHours.toInt()
val minutes = duration.standardMinutes.toInt()
val seconds = duration.standardSeconds.toInt()
val hours = duration.toHours().toInt()
val minutes = duration.toMinutes().toInt()
val seconds = duration.seconds
when {
seconds < 10 -> {
return resourceHelper.gs(R.string.omnipod_common_moments_ago)
@ -683,7 +655,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
// FIXME ideally we should just have access to LocalAlertUtils here
private fun getPumpUnreachableTimeout(): Duration {
return Duration.standardMinutes(
return Duration.ofMinutes(
sp.getInt(
R.string.key_pump_unreachable_threshold_minutes,
Constants.DEFAULT_PUMP_UNREACHABLE_THRESHOLD_MINUTES

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

View file

@ -8,12 +8,16 @@ import info.nightscout.androidaps.interfaces.ProfileFunction
import info.nightscout.androidaps.interfaces.PumpSync
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
import info.nightscout.androidaps.plugins.general.overview.events.EventDismissNotification
import info.nightscout.androidaps.plugins.general.overview.notifications.Notification
import info.nightscout.androidaps.plugins.pump.common.defs.PumpType
import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.activation.viewmodel.action.InsertCannulaViewModel
import info.nightscout.androidaps.plugins.pump.omnipod.dash.R
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram
import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Single
import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject
@ -23,6 +27,9 @@ class DashInsertCannulaViewModel @Inject constructor(
private val profileFunction: ProfileFunction,
private val pumpSync: PumpSync,
private val podStateManager: OmnipodDashPodStateManager,
private val rxBus: RxBusWrapper,
private val sp: SP,
injector: HasAndroidInjector,
logger: AAPSLogger
) : InsertCannulaViewModel(injector, logger) {
@ -45,7 +52,15 @@ class DashInsertCannulaViewModel @Inject constructor(
profile,
basalProgram
)
val disposable = omnipodManager.activatePodPart2(basalProgram).subscribeBy(
val expirationReminderEnabled = sp.getBoolean(R.string.key_omnipod_common_expiration_reminder_enabled, true)
val expirationHours = sp.getInt(R.string.key_omnipod_common_expiration_reminder_hours_before_shutdown, 9)
val expirationHoursBeforeShutdown = if (expirationReminderEnabled)
expirationHours.toLong()
else
null
val disposable = omnipodManager.activatePodPart2(basalProgram, expirationHoursBeforeShutdown).subscribeBy(
onNext = { podEvent ->
logger.debug(
LTag.PUMP,
@ -58,6 +73,7 @@ class DashInsertCannulaViewModel @Inject constructor(
},
onComplete = {
logger.debug("Pod activation part 2 completed")
podStateManager.basalProgram = basalProgram
pumpSync.connectNewPump()
pumpSync.insertTherapyEventIfNewWithTimestamp(
timestamp = System.currentTimeMillis(),
@ -71,6 +87,14 @@ class DashInsertCannulaViewModel @Inject constructor(
pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = podStateManager.uniqueId?.toString() ?: "n/a"
)
pumpSync.syncStopTemporaryBasalWithPumpId(
timestamp = System.currentTimeMillis(),
endPumpId = System.currentTimeMillis(),
pumpType = PumpType.OMNIPOD_DASH,
pumpSerial = podStateManager.uniqueId?.toString() ?: "n/a"
)
podStateManager.updateExpirationAlertSettings(expirationReminderEnabled, expirationHours)
rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_NOT_ATTACHED))
source.onSuccess(PumpEnactResult(injector).success(true))
}
)

View file

@ -65,11 +65,33 @@
validate:maxNumber="50"
validate:minNumber="5"
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
android:defaultValue="false"
android:key="@string/key_omnipod_common_automatically_silence_alerts_enabled"
android:title="@string/omnipod_common_preferences_automatically_silence_alerts" />
android:key="@string/key_omnipod_common_notification_uncertain_tbr_sound_enabled"
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>