Merge pull request #2311 from dv1/combov2-fixes-001
combov2: Fixes to Bluetooth permission checks, identical basal profile handling, pump errors
This commit is contained in:
commit
7e91b37d1d
4 changed files with 181 additions and 47 deletions
|
@ -41,6 +41,7 @@ class ComboV2Fragment : DaggerFragment() {
|
||||||
|
|
||||||
binding.combov2RefreshButton.setOnClickListener {
|
binding.combov2RefreshButton.setOnClickListener {
|
||||||
binding.combov2RefreshButton.isEnabled = false
|
binding.combov2RefreshButton.isEnabled = false
|
||||||
|
combov2Plugin.clearPumpErrorObservedFlag()
|
||||||
commandQueue.readStatus(rh.gs(info.nightscout.core.ui.R.string.user_request), null)
|
commandQueue.readStatus(rh.gs(info.nightscout.core.ui.R.string.user_request), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@ import info.nightscout.comboctl.parser.BatteryState
|
||||||
import info.nightscout.comboctl.parser.ReservoirState
|
import info.nightscout.comboctl.parser.ReservoirState
|
||||||
import info.nightscout.core.ui.dialogs.OKDialog
|
import info.nightscout.core.ui.dialogs.OKDialog
|
||||||
import info.nightscout.core.ui.toast.ToastUtils
|
import info.nightscout.core.ui.toast.ToastUtils
|
||||||
|
import info.nightscout.interfaces.AndroidPermission
|
||||||
|
import info.nightscout.interfaces.Config
|
||||||
import info.nightscout.interfaces.constraints.Constraint
|
import info.nightscout.interfaces.constraints.Constraint
|
||||||
import info.nightscout.interfaces.constraints.Constraints
|
import info.nightscout.interfaces.constraints.Constraints
|
||||||
import info.nightscout.interfaces.notifications.Notification
|
import info.nightscout.interfaces.notifications.Notification
|
||||||
|
@ -95,6 +97,8 @@ import info.nightscout.comboctl.base.Tbr as ComboCtlTbr
|
||||||
import info.nightscout.comboctl.main.Pump as ComboCtlPump
|
import info.nightscout.comboctl.main.Pump as ComboCtlPump
|
||||||
import info.nightscout.comboctl.main.PumpManager as ComboCtlPumpManager
|
import info.nightscout.comboctl.main.PumpManager as ComboCtlPumpManager
|
||||||
|
|
||||||
|
internal const val PUMP_ERROR_TIMEOUT_INTERVAL_MSECS = 1000L * 60 * 5
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ComboV2Plugin @Inject constructor (
|
class ComboV2Plugin @Inject constructor (
|
||||||
injector: HasAndroidInjector,
|
injector: HasAndroidInjector,
|
||||||
|
@ -107,7 +111,9 @@ class ComboV2Plugin @Inject constructor (
|
||||||
private val sp: SP,
|
private val sp: SP,
|
||||||
private val pumpSync: PumpSync,
|
private val pumpSync: PumpSync,
|
||||||
private val dateUtil: DateUtil,
|
private val dateUtil: DateUtil,
|
||||||
private val uiInteraction: UiInteraction
|
private val uiInteraction: UiInteraction,
|
||||||
|
private val androidPermission: AndroidPermission,
|
||||||
|
private val config: Config
|
||||||
) :
|
) :
|
||||||
PumpPluginBase(
|
PumpPluginBase(
|
||||||
PluginDescription()
|
PluginDescription()
|
||||||
|
@ -161,6 +167,13 @@ class ComboV2Plugin @Inject constructor (
|
||||||
private var lastConnectionTimestamp = 0L
|
private var lastConnectionTimestamp = 0L
|
||||||
private var lastComboAlert: AlertScreenContent? = null
|
private var lastComboAlert: AlertScreenContent? = null
|
||||||
|
|
||||||
|
// States for when the pump reports an error. We then want isInitialized()
|
||||||
|
// to return false until either the user presses the Refresh button or the
|
||||||
|
// pumpErrorTimeoutJob expires. That way, the loop won't run until then,
|
||||||
|
// giving the user a chance to handle the error.
|
||||||
|
private var pumpErrorObserved = false
|
||||||
|
private var pumpErrorTimeoutJob: Job? = null
|
||||||
|
|
||||||
// Set to true if a disconnect request came in while the driver
|
// Set to true if a disconnect request came in while the driver
|
||||||
// was in the Connecting, CheckingPump, or ExecutingCommand
|
// was in the Connecting, CheckingPump, or ExecutingCommand
|
||||||
// state (in other words, while isBusy() was returning true).
|
// state (in other words, while isBusy() was returning true).
|
||||||
|
@ -183,7 +196,7 @@ class ComboV2Plugin @Inject constructor (
|
||||||
private var activeBasalProfile: BasalProfile? = null
|
private var activeBasalProfile: BasalProfile? = null
|
||||||
// This is used for checking that the correct basal profile is
|
// This is used for checking that the correct basal profile is
|
||||||
// active in the Combo. If not, loop invocation is disallowed.
|
// active in the Combo. If not, loop invocation is disallowed.
|
||||||
// This is _not_ reset by disconect(). That's on purpose; it
|
// This is _not_ reset by disconnect(). That's on purpose; it
|
||||||
// is read by isLoopInvocationAllowed(), which is called even
|
// is read by isLoopInvocationAllowed(), which is called even
|
||||||
// if the pump is not connected.
|
// if the pump is not connected.
|
||||||
private var lastActiveBasalProfileNumber: Int? = null
|
private var lastActiveBasalProfileNumber: Int? = null
|
||||||
|
@ -277,6 +290,15 @@ class ComboV2Plugin @Inject constructor (
|
||||||
aapsLogger.debug(LTag.PUMP, "Creating bluetooth interface")
|
aapsLogger.debug(LTag.PUMP, "Creating bluetooth interface")
|
||||||
bluetoothInterface = AndroidBluetoothInterface(context)
|
bluetoothInterface = AndroidBluetoothInterface(context)
|
||||||
|
|
||||||
|
// Continue initialization in a separate coroutine. This allows us to call
|
||||||
|
// runWithPermissionCheck(), which will keep trying to run the code block
|
||||||
|
// until either the necessary Bluetooth permissios are granted, or the
|
||||||
|
// coroutine is cancelled (see onStop() below).
|
||||||
|
pumpCoroutineScope.launch {
|
||||||
|
runWithPermissionCheck(
|
||||||
|
context, config, aapsLogger, androidPermission,
|
||||||
|
permissionsToCheckFor = listOf("android.permission.BLUETOOTH_CONNECT")
|
||||||
|
) {
|
||||||
aapsLogger.debug(LTag.PUMP, "Setting up bluetooth interface")
|
aapsLogger.debug(LTag.PUMP, "Setting up bluetooth interface")
|
||||||
bluetoothInterface!!.setup()
|
bluetoothInterface!!.setup()
|
||||||
|
|
||||||
|
@ -301,8 +323,13 @@ class ComboV2Plugin @Inject constructor (
|
||||||
|
|
||||||
// NOTE: EventInitializationChanged is sent in getPumpStatus() .
|
// NOTE: EventInitializationChanged is sent in getPumpStatus() .
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
// Cancel any ongoing background coroutines. This includes an ongoing
|
||||||
|
// unfinished initialization that still waits for the user to grant
|
||||||
|
// Bluetooth permissions.
|
||||||
pumpCoroutineScope.cancel()
|
pumpCoroutineScope.cancel()
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
@ -381,7 +408,7 @@ class ComboV2Plugin @Inject constructor (
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isInitialized(): Boolean =
|
override fun isInitialized(): Boolean =
|
||||||
isPaired() && (driverStateFlow.value != DriverState.NotInitialized)
|
isPaired() && (driverStateFlow.value != DriverState.NotInitialized) && !pumpErrorObserved
|
||||||
|
|
||||||
override fun isSuspended(): Boolean =
|
override fun isSuspended(): Boolean =
|
||||||
when (driverStateUIFlow.value) {
|
when (driverStateUIFlow.value) {
|
||||||
|
@ -432,6 +459,16 @@ class ComboV2Plugin @Inject constructor (
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pumpErrorObserved) {
|
||||||
|
aapsLogger.debug(LTag.PUMP, "Aborting connect attempt since the pumpErrorObserved flag is set")
|
||||||
|
uiInteraction.addNotification(
|
||||||
|
Notification.COMBO_PUMP_ALARM,
|
||||||
|
text = rh.gs(R.string.combov2_cannot_connect_pump_error_observed),
|
||||||
|
level = Notification.NORMAL
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
when (driverStateFlow.value) {
|
when (driverStateFlow.value) {
|
||||||
DriverState.Connecting,
|
DriverState.Connecting,
|
||||||
DriverState.CheckingPump,
|
DriverState.CheckingPump,
|
||||||
|
@ -561,10 +598,15 @@ class ComboV2Plugin @Inject constructor (
|
||||||
var forciblyDisconnectDueToError = false
|
var forciblyDisconnectDueToError = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
runWithPermissionCheck(
|
||||||
|
context, config, aapsLogger, androidPermission,
|
||||||
|
permissionsToCheckFor = listOf("android.permission.BLUETOOTH_CONNECT")
|
||||||
|
) {
|
||||||
// Set maxNumAttempts to null to turn off the connection attempt limit inside the connect() call.
|
// Set maxNumAttempts to null to turn off the connection attempt limit inside the connect() call.
|
||||||
// The AAPS queue thread will anyway cause the connectionSetupJob to be canceled when its
|
// The AAPS queue thread will anyway cause the connectionSetupJob to be canceled when its
|
||||||
// connection timeout expires, so the Pump class' own connection attempt limiter is redundant.
|
// connection timeout expires, so the Pump class' own connection attempt limiter is redundant.
|
||||||
pump?.connect(maxNumAttempts = null)
|
pump?.connect(maxNumAttempts = null)
|
||||||
|
}
|
||||||
|
|
||||||
// No need to set the driver state here, since the pump's stateFlow will announce that.
|
// No need to set the driver state here, since the pump's stateFlow will announce that.
|
||||||
|
|
||||||
|
@ -744,18 +786,26 @@ class ComboV2Plugin @Inject constructor (
|
||||||
Notification.INFO,
|
Notification.INFO,
|
||||||
60
|
60
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
aapsLogger.debug(LTag.PUMP, "Basal profiles are equal; did not have to set anything")
|
||||||
|
// Treat this as if the command had been enacted. Setting a basal profile is
|
||||||
|
// an idempotent operation, meaning that setting the exact same profile factors
|
||||||
|
// twice in a row does not actually change anything. Therefore, we can just
|
||||||
|
// completely skip such a redundant set basal profile operation and still get
|
||||||
|
// the exact same result.
|
||||||
|
// Furthermore, it is actually important to also set enacted to true in this case
|
||||||
|
// because even though this _driver_ might know that the Combo uses this profile
|
||||||
|
// already, _AAPS_ might not. A good example is when AAPS is set up the first time
|
||||||
|
// and no profile has been activated. If in this case the profile happens to be
|
||||||
|
// identical to what's already in the Combo, then enacted=false would cause errors,
|
||||||
|
// because AAPS expects the driver to always enact the profile change in this case
|
||||||
|
// (since it thinks that no profile is set yet).
|
||||||
|
}
|
||||||
|
|
||||||
pumpEnactResult.apply {
|
pumpEnactResult.apply {
|
||||||
success = true
|
success = true
|
||||||
enacted = true
|
enacted = true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
aapsLogger.debug(LTag.PUMP, "Basal profiles are equal; did not have to set anything")
|
|
||||||
pumpEnactResult.apply {
|
|
||||||
success = true
|
|
||||||
enacted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
// Cancellation is not an error, but it also means
|
// Cancellation is not an error, but it also means
|
||||||
|
@ -1398,6 +1448,14 @@ class ComboV2Plugin @Inject constructor (
|
||||||
commandQueue.readStatus(reason, null)
|
commandQueue.readStatus(reason, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearPumpErrorObservedFlag() {
|
||||||
|
stopPumpErrorTimeout()
|
||||||
|
if (pumpErrorObserved) {
|
||||||
|
aapsLogger.info(LTag.PUMP, "Clearing pumpErrorObserved flag")
|
||||||
|
pumpErrorObserved = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*** Loop constraints ***/
|
/*** Loop constraints ***/
|
||||||
// These restrict the function of the loop in case of an event
|
// These restrict the function of the loop in case of an event
|
||||||
// that makes running a loop too risky, for example because something
|
// that makes running a loop too risky, for example because something
|
||||||
|
@ -1459,7 +1517,13 @@ class ComboV2Plugin @Inject constructor (
|
||||||
|
|
||||||
pairingJob = pumpCoroutineScope.async {
|
pairingJob = pumpCoroutineScope.async {
|
||||||
try {
|
try {
|
||||||
val pairingResult = pumpManager?.pairWithNewPump(discoveryDuration) { newPumpAddress, previousAttemptFailed ->
|
// Do the pairing attempt within runWithPermissionCheck()
|
||||||
|
// since pairing requires Bluetooth permissions.
|
||||||
|
val pairingResult = runWithPermissionCheck(
|
||||||
|
context, config, aapsLogger, androidPermission,
|
||||||
|
permissionsToCheckFor = listOf("android.permission.BLUETOOTH_CONNECT")
|
||||||
|
) {
|
||||||
|
pumpManager?.pairWithNewPump(discoveryDuration) { newPumpAddress, previousAttemptFailed ->
|
||||||
aapsLogger.info(
|
aapsLogger.info(
|
||||||
LTag.PUMP,
|
LTag.PUMP,
|
||||||
"New pairing PIN request from Combo pump with Bluetooth " +
|
"New pairing PIN request from Combo pump with Bluetooth " +
|
||||||
|
@ -1468,6 +1532,7 @@ class ComboV2Plugin @Inject constructor (
|
||||||
_previousPairingAttemptFailedFlow.value = previousAttemptFailed
|
_previousPairingAttemptFailedFlow.value = previousAttemptFailed
|
||||||
newPINChannel.receive()
|
newPINChannel.receive()
|
||||||
} ?: throw IllegalStateException("Attempting to access uninitialized pump manager")
|
} ?: throw IllegalStateException("Attempting to access uninitialized pump manager")
|
||||||
|
}
|
||||||
|
|
||||||
if (pairingResult !is ComboCtlPumpManager.PairingResult.Success)
|
if (pairingResult !is ComboCtlPumpManager.PairingResult.Success)
|
||||||
return@async
|
return@async
|
||||||
|
@ -1550,6 +1615,8 @@ class ComboV2Plugin @Inject constructor (
|
||||||
_serialNumberUIFlow.value = ""
|
_serialNumberUIFlow.value = ""
|
||||||
_bluetoothAddressUIFlow.value = ""
|
_bluetoothAddressUIFlow.value = ""
|
||||||
|
|
||||||
|
clearPumpErrorObservedFlag()
|
||||||
|
|
||||||
// The unpairing variable is set to false in
|
// The unpairing variable is set to false in
|
||||||
// the PumpManager onPumpUnpaired callback.
|
// the PumpManager onPumpUnpaired callback.
|
||||||
}
|
}
|
||||||
|
@ -1748,6 +1815,23 @@ class ComboV2Plugin @Inject constructor (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startPumpErrorTimeout() {
|
||||||
|
if (pumpErrorTimeoutJob != null)
|
||||||
|
return
|
||||||
|
|
||||||
|
pumpErrorTimeoutJob = pumpCoroutineScope.launch {
|
||||||
|
delay(PUMP_ERROR_TIMEOUT_INTERVAL_MSECS)
|
||||||
|
aapsLogger.info(LTag.PUMP, "Clearing pumpErrorObserved flag after timeout was reached")
|
||||||
|
pumpErrorObserved = false
|
||||||
|
commandQueue.readStatus(rh.gs(R.string.combov2_refresh_pump_status_after_error), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopPumpErrorTimeout() {
|
||||||
|
pumpErrorTimeoutJob?.cancel()
|
||||||
|
pumpErrorTimeoutJob = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateBaseBasalRateUI() {
|
private fun updateBaseBasalRateUI() {
|
||||||
val currentHour = DateTime().hourOfDay().get()
|
val currentHour = DateTime().hourOfDay().get()
|
||||||
// This sets value to null if no profile is set,
|
// This sets value to null if no profile is set,
|
||||||
|
@ -2142,6 +2226,12 @@ class ComboV2Plugin @Inject constructor (
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyAboutComboAlert(alert: AlertScreenContent) {
|
private fun notifyAboutComboAlert(alert: AlertScreenContent) {
|
||||||
|
if (alert is AlertScreenContent.Error) {
|
||||||
|
aapsLogger.info(LTag.PUMP, "Error screen observed - setting pumpErrorObserved flag")
|
||||||
|
pumpErrorObserved = true
|
||||||
|
startPumpErrorTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
uiInteraction.addNotification(
|
uiInteraction.addNotification(
|
||||||
Notification.COMBO_PUMP_ALARM,
|
Notification.COMBO_PUMP_ALARM,
|
||||||
text = "${rh.gs(R.string.combov2_combo_alert)}: ${getAlertDescription(alert)}",
|
text = "${rh.gs(R.string.combov2_combo_alert)}: ${getAlertDescription(alert)}",
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
package info.nightscout.pump.combov2
|
package info.nightscout.pump.combov2
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import info.nightscout.comboctl.android.AndroidBluetoothPermissionException
|
||||||
import info.nightscout.comboctl.main.BasalProfile
|
import info.nightscout.comboctl.main.BasalProfile
|
||||||
import info.nightscout.comboctl.main.NUM_COMBO_BASAL_PROFILE_FACTORS
|
import info.nightscout.comboctl.main.NUM_COMBO_BASAL_PROFILE_FACTORS
|
||||||
|
import info.nightscout.interfaces.AndroidPermission
|
||||||
|
import info.nightscout.interfaces.Config
|
||||||
import info.nightscout.interfaces.profile.Profile as AAPSProfile
|
import info.nightscout.interfaces.profile.Profile as AAPSProfile
|
||||||
|
import info.nightscout.rx.logging.AAPSLogger
|
||||||
|
import info.nightscout.rx.logging.LTag
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
// Utility extension functions for clearer conversion between
|
// Utility extension functions for clearer conversion between
|
||||||
// ComboCtl units and AAPS units. ComboCtl uses integer-encoded
|
// ComboCtl units and AAPS units. ComboCtl uses integer-encoded
|
||||||
|
@ -23,3 +31,36 @@ fun AAPSProfile.toComboCtlBasalProfile(): BasalProfile {
|
||||||
}
|
}
|
||||||
return BasalProfile(factors)
|
return BasalProfile(factors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> runWithPermissionCheck(
|
||||||
|
context: Context,
|
||||||
|
config: Config,
|
||||||
|
aapsLogger: AAPSLogger,
|
||||||
|
androidPermission: AndroidPermission,
|
||||||
|
permissionsToCheckFor: Collection<String>,
|
||||||
|
block: suspend () -> T
|
||||||
|
): T {
|
||||||
|
var permissions = permissionsToCheckFor
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
if (config.PUMPDRIVERS && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val notAllPermissionsGranted = permissions.fold(initial = false) { currentResult, permission ->
|
||||||
|
return@fold if (androidPermission.permissionNotGranted(context, permission)) {
|
||||||
|
aapsLogger.debug(LTag.PUMP, "permission $permission was not granted by the user")
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
currentResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notAllPermissionsGranted) {
|
||||||
|
delay(1000) // Wait a little bit before retrying to avoid 100% CPU usage
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return block.invoke()
|
||||||
|
} catch (e: AndroidBluetoothPermissionException) {
|
||||||
|
permissions = permissionsToCheckFor union e.missingPermissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -135,4 +135,6 @@ buttons at the same time to cancel pairing)\n
|
||||||
<string name="combov2_datetime_changed">Date and/or time changed</string>
|
<string name="combov2_datetime_changed">Date and/or time changed</string>
|
||||||
<string name="combov2_dst_started">Daylight savings time (DST) started</string>
|
<string name="combov2_dst_started">Daylight savings time (DST) started</string>
|
||||||
<string name="combov2_dst_ended">Daylight savings time (DST) ended</string>
|
<string name="combov2_dst_ended">Daylight savings time (DST) ended</string>
|
||||||
|
<string name="combov2_cannot_connect_pump_error_observed">Cannot connect to pump because the pump reported an error. User must handle the error and then either wait 5 minutes or press the Refresh button in the driver tab.</string>
|
||||||
|
<string name="combov2_refresh_pump_status_after_error">Refreshing pump status after the pump reported an error</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue