Merge pull request #2457 from dv1/combov2-fixes-001

combov2: Fixes for the refresh button, unsafe casts, crashes due to Bluetooth, improved warning screen handling, and additional language support
This commit is contained in:
Milos Kozak 2023-03-13 22:21:45 +01:00 committed by GitHub
commit 294f3cb789
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 4768 additions and 1544 deletions

View file

@ -9,6 +9,8 @@ import info.nightscout.interfaces.userEntry.ValueWithUnitMapper
interface UserEntryLogger {
fun log(action: Action, source: Sources, note: String?, timestamp: Long, vararg listValues: ValueWithUnit?)
fun log(action: Action, source: Sources, note: String?, timestamp: Long, listValues: List<ValueWithUnit?>)
fun log(action: Action, source: Sources, note: String? = "", vararg listValues: ValueWithUnit?)
fun log(action: Action, source: Sources, vararg listValues: ValueWithUnit?)
fun log(action: Action, source: Sources, note: String? = "", listValues: List<ValueWithUnit?> = listOf())

View file

@ -133,6 +133,7 @@ open class Notification {
const val EOELOW_PATCH_ALERTS = 79
const val COMBO_PUMP_SUSPENDED = 80
const val COMBO_UNKNOWN_TBR = 81
const val BLUETOOTH_NOT_ENABLED = 82
const val USER_MESSAGE = 1000

View file

@ -29,16 +29,14 @@ class UserEntryLoggerImpl @Inject constructor(
private val compositeDisposable = CompositeDisposable()
override fun log(action: Action, source: Sources, note: String?, vararg listValues: ValueWithUnit?) = log(action, source, note, listValues.toList())
override fun log(action: Action, source: Sources, note: String?, timestamp: Long, vararg listValues: ValueWithUnit?) = log(action, source, note, timestamp, listValues.toList())
override fun log(action: Action, source: Sources, vararg listValues: ValueWithUnit?) = log(action, source, "", listValues.toList())
override fun log(action: Action, source: Sources, note: String?, listValues: List<ValueWithUnit?>) {
override fun log(action: Action, source: Sources, note: String?, timestamp: Long, listValues: List<ValueWithUnit?>) {
val filteredValues = listValues.toList().filterNotNull()
log(
listOf(
UserEntry(
timestamp = dateUtil.now(),
timestamp = timestamp,
action = action,
source = source,
note = note ?: "",
@ -48,6 +46,12 @@ class UserEntryLoggerImpl @Inject constructor(
)
}
override fun log(action: Action, source: Sources, note: String?, vararg listValues: ValueWithUnit?) = log(action, source, note, listValues.toList())
override fun log(action: Action, source: Sources, vararg listValues: ValueWithUnit?) = log(action, source, "", listValues.toList())
override fun log(action: Action, source: Sources, note: String?, listValues: List<ValueWithUnit?>) = log(action, source, note, dateUtil.now(), listValues)
override fun log(entries: List<UserEntry>) {
compositeDisposable += repository.runTransactionForResult(UserEntryTransaction(entries))
.subscribeOn(aapsSchedulers.io)

View file

@ -273,7 +273,13 @@ class PumpSyncImplementation @Inject constructor(
pumpSerial = pumpSerial
)
)
uel.log(UserEntry.Action.CAREPORTAL, pumpType.source.toDbSource(), note, ValueWithUnit.Timestamp(timestamp), ValueWithUnit.TherapyEventType(type.toDBbEventType()))
uel.log(
action = UserEntry.Action.CAREPORTAL,
source = pumpType.source.toDbSource(),
note = note,
timestamp = timestamp,
ValueWithUnit.Timestamp(timestamp), ValueWithUnit.TherapyEventType(type.toDBbEventType())
)
repository.runTransactionForResult(InsertIfNewByTimestampTherapyEventTransaction(therapyEvent))
.doOnError {
aapsLogger.error(LTag.DATABASE, "Error while saving TherapyEvent", it)

View file

@ -7,8 +7,10 @@ import android.content.Intent
import android.content.IntentFilter
import info.nightscout.comboctl.base.BluetoothAddress
import info.nightscout.comboctl.base.BluetoothDevice
import info.nightscout.comboctl.base.BluetoothNotEnabledException
import info.nightscout.comboctl.base.BluetoothException
import info.nightscout.comboctl.base.BluetoothInterface
import info.nightscout.comboctl.base.BluetoothNotAvailableException
import info.nightscout.comboctl.base.LogLevel
import info.nightscout.comboctl.base.Logger
import info.nightscout.comboctl.base.toBluetoothAddress
@ -35,7 +37,10 @@ private val logger = Logger.get("AndroidBluetoothInterface")
* instance is an ideal choice.
*/
class AndroidBluetoothInterface(private val androidContext: Context) : BluetoothInterface {
private var bluetoothAdapter: SystemBluetoothAdapter? = null
private var _bluetoothAdapter: SystemBluetoothAdapter? = null
private val bluetoothAdapter: SystemBluetoothAdapter
get() = _bluetoothAdapter ?: throw BluetoothNotAvailableException()
private var rfcommServerSocket: SystemBluetoothServerSocket? = null
private var discoveryStarted = false
private var discoveryBroadcastReceiver: BroadcastReceiver? = null
@ -96,11 +101,16 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
else @Suppress("DEPRECATION") getParcelableExtra(name)
fun setup() {
val bluetoothManager = androidContext.getSystemService(Context.BLUETOOTH_SERVICE) as SystemBluetoothManager
bluetoothAdapter = bluetoothManager.adapter
val bluetoothManager = androidContext.getSystemService(Context.BLUETOOTH_SERVICE) as? SystemBluetoothManager
_bluetoothAdapter = bluetoothManager?.adapter
checkIfBluetoothEnabledAndAvailable()
val bondedDevices = checkForConnectPermission(androidContext) {
bluetoothAdapter!!.bondedDevices
// The "not enabled" check above is important, because in the disabled
// state, the adapter returns an empty list here. This would mislead
// the logic below into thinking that there are no bonded devices.
bluetoothAdapter.bondedDevices
}
logger(LogLevel.DEBUG) { "Found ${bondedDevices.size} bonded Bluetooth device(s)" }
@ -180,7 +190,7 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
// necessary for correct function, just a detail for sake of completeness.)
logger(LogLevel.DEBUG) { "Setting up RFCOMM listener socket" }
rfcommServerSocket = checkForConnectPermission(androidContext) {
bluetoothAdapter!!.listenUsingInsecureRfcommWithServiceRecord(
bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
sdpServiceName,
Constants.sdpSerialPortUUID
)
@ -203,7 +213,7 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
logger(LogLevel.DEBUG) { "Closing accepted incoming RFCOMM socket" }
try {
socket.close()
} catch (e: IOException) {
} catch (_: IOException) {
}
}
}
@ -274,20 +284,24 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
stopDiscoveryInternal()
}
override fun getDevice(deviceAddress: BluetoothAddress): BluetoothDevice =
AndroidBluetoothDevice(androidContext, bluetoothAdapter!!, deviceAddress)
override fun getDevice(deviceAddress: BluetoothAddress): BluetoothDevice {
checkIfBluetoothEnabledAndAvailable()
return AndroidBluetoothDevice(androidContext, bluetoothAdapter, deviceAddress)
}
override fun getAdapterFriendlyName() =
checkForConnectPermission(androidContext) { bluetoothAdapter!!.name }
checkForConnectPermission(androidContext) { bluetoothAdapter.name }
?: throw BluetoothException("Could not get Bluetooth adapter friendly name")
override fun getPairedDeviceAddresses(): Set<BluetoothAddress> =
try {
override fun getPairedDeviceAddresses(): Set<BluetoothAddress> {
checkIfBluetoothEnabledAndAvailable()
return try {
deviceAddressLock.lock()
pairedDeviceAddresses.filter { pairedDeviceAddress -> deviceFilterCallback(pairedDeviceAddress) }.toSet()
} finally {
deviceAddressLock.unlock()
}
}
private fun stopDiscoveryInternal() {
// Close the server socket. This frees RFCOMM resources and ends
@ -332,6 +346,16 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
}
}
private fun checkIfBluetoothEnabledAndAvailable() {
// Trying to access bluetoothAdapter here if it is currently null will
// automatically cause BluetoothNotAvailableException to be thrown,
// so that case is also covered implicitly by this code.
if (!bluetoothAdapter.isEnabled || (bluetoothAdapter.state != SystemBluetoothAdapter.STATE_ON)) {
logger(LogLevel.ERROR) { "Bluetooth is not enabled" }
throw BluetoothNotEnabledException()
}
}
private fun onAclConnected(intent: Intent, foundNewPairedDevice: (deviceAddress: BluetoothAddress) -> Unit) {
// Sanity check in case we get this notification for the
// device already and need to avoid duplicate processing.

View file

@ -23,3 +23,19 @@ open class BluetoothPermissionException(message: String?, cause: Throwable?) : B
constructor(message: String) : this(message, null)
constructor(cause: Throwable) : this(null, cause)
}
/**
* Exception thrown when trying to use Bluetooth even though the adapter is not enabled.
*
* Note that unlike [BluetoothNotAvailableException], here, the adapter _does_ exist,
* and is just currently turned off.
*/
open class BluetoothNotEnabledException : BluetoothException("Bluetooth is not enabled")
/**
* Exception thrown when trying to use Bluetooth even though there no adapter available.
*
* "Not available" typically means that the platform has no Bluetooth hardware, or that
* said hardware is inaccessible.
*/
open class BluetoothNotAvailableException : BluetoothException("Bluetooth is not available - there is no usable adapter")

View file

@ -140,6 +140,10 @@ interface BluetoothInterface {
* a Bluetooth subsystem that has been shut down.
* @throws BluetoothPermissionException if discovery fails because
* scanning and connection permissions are missing.
* @throws BluetoothNotEnabledException if the system's
* Bluetooth adapter is currently not enabled.
* @throws BluetoothNotAvailableException if the system's
* Bluetooth adapter is currently not available.
* @throws BluetoothException if discovery fails due to an underlying
* Bluetooth issue.
*/
@ -172,6 +176,10 @@ interface BluetoothInterface {
*
* @return BluetoothDevice instance for the device with the
* given address
* @throws BluetoothNotEnabledException if the system's
* Bluetooth adapter is currently not enabled.
* @throws BluetoothNotAvailableException if the system's
* Bluetooth adapter is currently not available.
* @throws IllegalStateException if the interface is in a state
* in which accessing devices is not possible, such as
* a Bluetooth subsystem that has been shut down.
@ -183,6 +191,8 @@ interface BluetoothInterface {
*
* @throws BluetoothPermissionException if getting the adapter name
* fails because connection permissions are missing.
* @throws BluetoothNotAvailableException if the system's
* Bluetooth adapter is currently not available.
* @throws BluetoothException if getting the adapter name fails
* due to an underlying Bluetooth issue.
*/
@ -205,6 +215,11 @@ interface BluetoothInterface {
* round, it is possible that between the [getPairedDeviceAddresses]
* call and the [onDeviceUnpaired] assignment, a device is
* unpaired, and thus does not get noticed.
*
* @throws BluetoothNotEnabledException if the system's
* Bluetooth adapter is currently not enabled.
* @throws BluetoothNotAvailableException if the system's
* Bluetooth adapter is currently not available.
*/
fun getPairedDeviceAddresses(): Set<BluetoothAddress>
}

View file

@ -249,6 +249,7 @@ class Pump(
// Used for keeping track of wether an RT alert screen was already dismissed
// (necessary since the screen may change its contents but still be the same screen).
private var rtScreenAlreadyDismissed = false
private var seenAlertAfterDismissingCounter = 0
// Used in handleAlertScreenContent() to check if the current alert
// screen contains the same alert as the previous one.
private var lastObservedAlertScreenContent: AlertScreenContent? = null
@ -2401,10 +2402,32 @@ class Pump(
// the two button presses, so there is no need to wait
// for the second screen - just press twice right away.
if (!rtScreenAlreadyDismissed) {
logger(LogLevel.DEBUG) { "Dismissing W$warningCode by short-pressing CHECK twice" }
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK)
val numRequiredButtonPresses = when (alertScreenContent.state) {
AlertScreenContent.AlertScreenState.TO_SNOOZE -> 2
AlertScreenContent.AlertScreenState.TO_CONFIRM -> 1
else -> throw AlertScreenException(alertScreenContent)
}
logger(LogLevel.DEBUG) { "Dismissing W$warningCode by short-pressing CHECK $numRequiredButtonPresses time(s)" }
for (i in 1..numRequiredButtonPresses)
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK)
rtScreenAlreadyDismissed = true
} else {
// In rare cases, an alert screen may still show after an alert
// was dismissed. Unfortunately, it is not immediately clear if
// this is the case, because the RT screen updates can come in
// with some temporal jitter. So, to be safe, we only begin to
// again handle alert screens after >10 alert screens were
// observed in sequence.
logger(LogLevel.DEBUG) { "W$warningCode already dismissed" }
seenAlertAfterDismissingCounter++
if (seenAlertAfterDismissingCounter > 10) {
logger(LogLevel.WARN) {
"Saw an alert screen $seenAlertAfterDismissingCounter time(s) " +
"after having dismissed an alert twice; now again handling alerts"
}
rtScreenAlreadyDismissed = false
seenAlertAfterDismissingCounter = 0
}
}
}
}

View file

@ -81,8 +81,17 @@ sealed class MainScreenContent {
* Possible contents of alert (= warning/error) screens.
*/
sealed class AlertScreenContent {
data class Warning(val code: Int) : AlertScreenContent()
data class Error(val code: Int) : AlertScreenContent()
enum class AlertScreenState {
TO_SNOOZE,
TO_CONFIRM,
// Used when the alert is an error. The text in error screens is not
// interpreted, since it is anyway fully up to the user to interpret it.
ERROR_TEXT,
HISTORY_ENTRY
}
data class Warning(val code: Int, val state: AlertScreenState) : AlertScreenContent()
data class Error(val code: Int, val state: AlertScreenState) : AlertScreenContent()
/**
* "Content" while the alert symbol & code currently are "blinked out".
@ -1139,8 +1148,9 @@ class AlertScreenParser : Parser() {
OptionalParser(SingleGlyphTypeParser(Glyph.LargeSymbol::class)), // warning/error symbol
OptionalParser(SingleGlyphTypeParser(Glyph.LargeCharacter::class)), // "W" or "E"
OptionalParser(IntegerParser()), // warning/error number
OptionalParser(SingleGlyphTypeParser(Glyph.LargeSymbol::class)), // stop symbol (only with errors)
SingleGlyphParser(Glyph.SmallSymbol(SmallSymbol.CHECK))
OptionalParser(SingleGlyphTypeParser(Glyph.LargeSymbol::class)), // stop symbol (shown in suspended state)
SingleGlyphParser(Glyph.SmallSymbol(SmallSymbol.CHECK)),
StringParser() // snooze / confirm text
)
).parse(parseContext)
@ -1151,14 +1161,27 @@ class AlertScreenParser : Parser() {
return when (parseResult.valueAtOrNull<Glyph>(0)) {
Glyph.LargeSymbol(LargeSymbol.WARNING) -> {
val stateString = parseResult.valueAt<String>(4)
val alertState = when (knownScreenTitles[stateString]) {
TitleID.ALERT_TO_SNOOZE -> AlertScreenContent.AlertScreenState.TO_SNOOZE
TitleID.ALERT_TO_CONFIRM -> AlertScreenContent.AlertScreenState.TO_CONFIRM
else -> return ParseResult.Failed
}
ParseResult.Value(ParsedScreen.AlertScreen(
AlertScreenContent.Warning(parseResult.valueAt(2))
AlertScreenContent.Warning(parseResult.valueAt(2), alertState)
))
}
Glyph.LargeSymbol(LargeSymbol.ERROR) -> {
ParseResult.Value(ParsedScreen.AlertScreen(
AlertScreenContent.Error(parseResult.valueAt(2))
AlertScreenContent.Error(
parseResult.valueAt(2),
// We don't really care about the state string if an error is shown.
// It's not like any logic here will interpret it; that text is
// purely for the user. So, don't bother interpreting it here, and
// just assign a generic ERROR_TEXT state value instead.
AlertScreenContent.AlertScreenState.ERROR_TEXT
)
))
}
@ -1226,6 +1249,7 @@ class TemporaryBasalRatePercentageScreenParser : Parser() {
override fun parseImpl(parseContext: ParseContext): ParseResult {
val parseResult = SequenceParser(
listOf(
OptionalParser(SingleGlyphParser(Glyph.SmallSymbol(SmallSymbol.PERCENT))),
SingleGlyphParser(Glyph.LargeSymbol(LargeSymbol.BASAL)),
OptionalParser(IntegerParser()), // TBR percentage
SingleGlyphParser(Glyph.LargeSymbol(LargeSymbol.PERCENT)),
@ -1654,7 +1678,10 @@ class MyDataErrorDataScreenParser : Parser() {
index = index,
totalNumEntries = totalNumEntries,
timestamp = timestamp,
alert = if (alertType == SmallSymbol.WARNING) AlertScreenContent.Warning(alertNumber) else AlertScreenContent.Error(alertNumber)
alert = if (alertType == SmallSymbol.WARNING)
AlertScreenContent.Warning(alertNumber, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
else
AlertScreenContent.Error(alertNumber, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
)
}

View file

@ -1414,6 +1414,15 @@ val glyphPatterns = mapOf<Glyph, Pattern>(
"",
"█████"
)),
Glyph.SmallCharacter('Ė') to Pattern(arrayOf(
"",
" ",
"█████",
"",
"████ ",
"",
"█████"
)),
Glyph.SmallCharacter('ę') to Pattern(arrayOf(
"█████",
"",

View file

@ -19,7 +19,9 @@ enum class TitleID {
BOLUS_DATA,
ERROR_DATA,
DAILY_TOTALS,
TBR_DATA
TBR_DATA,
ALERT_TO_SNOOZE,
ALERT_TO_CONFIRM
}
/**
@ -46,6 +48,8 @@ val knownScreenTitles = mapOf(
"ERROR DATA" to TitleID.ERROR_DATA,
"DAILY TOTALS" to TitleID.DAILY_TOTALS,
"TBR DATA" to TitleID.TBR_DATA,
"TO SNOOZE" to TitleID.ALERT_TO_SNOOZE,
"TO CONFIRM" to TitleID.ALERT_TO_CONFIRM,
// Spanish
"QUICK INFO" to TitleID.QUICK_INFO,
@ -60,6 +64,8 @@ val knownScreenTitles = mapOf(
"DATOS DE ERROR" to TitleID.ERROR_DATA,
"TOTALES DIARIOS" to TitleID.DAILY_TOTALS,
"DATOS DE DBT" to TitleID.TBR_DATA,
"REPETIR SEÑAL" to TitleID.ALERT_TO_SNOOZE,
"CONFIRMAR" to TitleID.ALERT_TO_CONFIRM,
// French
"QUICK INFO" to TitleID.QUICK_INFO,
@ -74,6 +80,8 @@ val knownScreenTitles = mapOf(
"ERREURS" to TitleID.ERROR_DATA,
"QUANTITÉS JOURN." to TitleID.DAILY_TOTALS,
"DBT" to TitleID.TBR_DATA,
"RAPPEL TARD" to TitleID.ALERT_TO_SNOOZE, // actually, the text is "RAPPEL + TARD", but the + symbol is ignored to simplify parsing
"POUR CONFIRMER" to TitleID.ALERT_TO_CONFIRM,
// Italian
"QUICK INFO" to TitleID.QUICK_INFO,
@ -88,6 +96,8 @@ val knownScreenTitles = mapOf(
"MEMORIA ALLARMI" to TitleID.ERROR_DATA,
"TOTALI GIORNATA" to TitleID.DAILY_TOTALS,
"MEMORIA PBT" to TitleID.TBR_DATA,
"RIPETI ALLARME" to TitleID.ALERT_TO_SNOOZE,
"PER CONFERMARE" to TitleID.ALERT_TO_CONFIRM,
// Russian
"QUICK INFO" to TitleID.QUICK_INFO,
@ -102,6 +112,8 @@ val knownScreenTitles = mapOf(
"ДАHHЫE OБ O ИБ." to TitleID.ERROR_DATA,
"CУTOЧHЫE ДOЗЫ" to TitleID.DAILY_TOTALS,
"ДАHHЫE O BБC" to TitleID.TBR_DATA,
"BЫKЛ. ЗBУK" to TitleID.ALERT_TO_SNOOZE,
"ПOДTBEPДИTЬ" to TitleID.ALERT_TO_CONFIRM,
// Turkish
"QUICK INFO" to TitleID.QUICK_INFO,
@ -116,6 +128,8 @@ val knownScreenTitles = mapOf(
"HATA VERİLERİ" to TitleID.ERROR_DATA,
"GÜNLÜK TOPLAM" to TitleID.DAILY_TOTALS,
"GBH VERİLERİ" to TitleID.TBR_DATA,
"ERTELE" to TitleID.ALERT_TO_SNOOZE,
"ONAYLA" to TitleID.ALERT_TO_CONFIRM,
// Polish
"QUICK INFO" to TitleID.QUICK_INFO,
@ -130,6 +144,8 @@ val knownScreenTitles = mapOf(
"DANE BŁĘDU" to TitleID.ERROR_DATA,
"DZIEN. D. CAŁK." to TitleID.DAILY_TOTALS,
"DANE TDP" to TitleID.TBR_DATA,
"ABY WYCISZYĆ" to TitleID.ALERT_TO_SNOOZE,
"ABY POTWIERDZ." to TitleID.ALERT_TO_CONFIRM,
// Czech
"QUICK INFO" to TitleID.QUICK_INFO,
@ -144,6 +160,8 @@ val knownScreenTitles = mapOf(
"ÚDAJE CHYB" to TitleID.ERROR_DATA,
"CELK. DEN. DÁVKY" to TitleID.DAILY_TOTALS,
"ÚDAJE DBD" to TitleID.TBR_DATA,
"ODLOŽIT" to TitleID.ALERT_TO_SNOOZE,
"POTVRDIT" to TitleID.ALERT_TO_CONFIRM,
// Hungarian
"QUICK INFO" to TitleID.QUICK_INFO,
@ -158,6 +176,8 @@ val knownScreenTitles = mapOf(
"HIBAADATOK" to TitleID.ERROR_DATA,
"NAPI TELJES" to TitleID.DAILY_TOTALS,
"TBR-ADATOK" to TitleID.TBR_DATA,
"NÉMÍTÁS" to TitleID.ALERT_TO_SNOOZE,
"JÓVÁHAGYÁS" to TitleID.ALERT_TO_CONFIRM,
// Slovak
"QUICK INFO" to TitleID.QUICK_INFO,
@ -172,6 +192,8 @@ val knownScreenTitles = mapOf(
"DÁTA O CHYBÁCH" to TitleID.ERROR_DATA,
"SÚČTY DŇA" to TitleID.DAILY_TOTALS,
"DBD DÁTA" to TitleID.TBR_DATA,
"STLMI" to TitleID.ALERT_TO_SNOOZE,
"POTVRDI" to TitleID.ALERT_TO_CONFIRM,
// Romanian
"QUICK INFO" to TitleID.QUICK_INFO,
@ -186,6 +208,8 @@ val knownScreenTitles = mapOf(
"DATE EROARE" to TitleID.ERROR_DATA,
"TOTALURI ZILNICE" to TitleID.DAILY_TOTALS,
"DATE RBT" to TitleID.TBR_DATA,
"OPRIRE SONERIE" to TitleID.ALERT_TO_SNOOZE,
"CONFIRMARE" to TitleID.ALERT_TO_CONFIRM,
// Croatian
"QUICK INFO" to TitleID.QUICK_INFO,
@ -200,6 +224,8 @@ val knownScreenTitles = mapOf(
"PODACI O GREŠK." to TitleID.ERROR_DATA,
"UKUPNE DNEV.DOZE" to TitleID.DAILY_TOTALS,
"PODACI O PBD-U" to TitleID.TBR_DATA,
"ZA ODGODU" to TitleID.ALERT_TO_SNOOZE,
"ZA POTVRDU" to TitleID.ALERT_TO_CONFIRM,
// Dutch
"QUICK INFO" to TitleID.QUICK_INFO,
@ -214,6 +240,8 @@ val knownScreenTitles = mapOf(
"FOUTENGEGEVENS" to TitleID.ERROR_DATA,
"DAGTOTALEN" to TitleID.DAILY_TOTALS,
"TBD-GEGEVENS" to TitleID.TBR_DATA,
"UITSTELLEN" to TitleID.ALERT_TO_SNOOZE,
"BEVESTIGEN" to TitleID.ALERT_TO_CONFIRM,
// Greek
"QUICK INFO" to TitleID.QUICK_INFO,
@ -228,6 +256,8 @@ val knownScreenTitles = mapOf(
"ΔEΔOМ. ΣΦАΛМАTΩN" to TitleID.ERROR_DATA,
"HМEPHΣIO ΣΥNOΛO" to TitleID.DAILY_TOTALS,
"ΔEΔOМENА П.B.P." to TitleID.TBR_DATA,
"ANAΣTOΛH" to TitleID.ALERT_TO_SNOOZE,
"EПIBEBАIΩΣH" to TitleID.ALERT_TO_CONFIRM,
// Finnish
"QUICK INFO" to TitleID.QUICK_INFO,
@ -242,6 +272,8 @@ val knownScreenTitles = mapOf(
"HÄLYTYSTIEDOT" to TitleID.ERROR_DATA,
"PÄIV. KOK.ANNOS" to TitleID.DAILY_TOTALS,
"TBA - TIEDOT" to TitleID.TBR_DATA,
"ILMOITA MYÖH." to TitleID.ALERT_TO_SNOOZE,
"VAHVISTA" to TitleID.ALERT_TO_CONFIRM,
// Norwegian
"QUICK INFO" to TitleID.QUICK_INFO,
@ -256,6 +288,8 @@ val knownScreenTitles = mapOf(
"FEILDATA" to TitleID.ERROR_DATA,
"DØGNMENGDE" to TitleID.DAILY_TOTALS,
"MBD-DATA" to TitleID.TBR_DATA,
"FOR Å SLUMRE" to TitleID.ALERT_TO_SNOOZE,
"FOR Å BEKREFTE" to TitleID.ALERT_TO_CONFIRM,
// Portuguese
"QUICK INFO" to TitleID.QUICK_INFO,
@ -271,6 +305,8 @@ val knownScreenTitles = mapOf(
"DADOS DE ERROS" to TitleID.ERROR_DATA, "DADOS DE ALARMES" to TitleID.ERROR_DATA,
"TOTAIS DIÁRIOS" to TitleID.DAILY_TOTALS,
"DADOS DBT" to TitleID.TBR_DATA,
"PARA SILENCIAR" to TitleID.ALERT_TO_SNOOZE,
"PARA CONFIRMAR" to TitleID.ALERT_TO_CONFIRM,
// Swedish
"QUICK INFO" to TitleID.QUICK_INFO,
@ -285,6 +321,8 @@ val knownScreenTitles = mapOf(
"FELDATA" to TitleID.ERROR_DATA,
"DYGNSHISTORIK" to TitleID.DAILY_TOTALS,
"TBD DATA" to TitleID.TBR_DATA,
"SNOOZE" to TitleID.ALERT_TO_SNOOZE,
"BEKRÄFTA" to TitleID.ALERT_TO_CONFIRM,
// Danish
"QUICK INFO" to TitleID.QUICK_INFO,
@ -299,6 +337,8 @@ val knownScreenTitles = mapOf(
"FEJLDATA" to TitleID.ERROR_DATA,
"DAGLIG TOTAL" to TitleID.DAILY_TOTALS,
"MBR-DATA" to TitleID.TBR_DATA,
"FOR AT UDSÆTTE" to TitleID.ALERT_TO_SNOOZE,
"FOR GODKEND" to TitleID.ALERT_TO_CONFIRM,
// German
"QUICK INFO" to TitleID.QUICK_INFO,
@ -313,6 +353,40 @@ val knownScreenTitles = mapOf(
"FEHLERMELDUNGEN" to TitleID.ERROR_DATA,
"TAGESGESAMTMENGE" to TitleID.DAILY_TOTALS,
"TBR-INFORMATION" to TitleID.TBR_DATA,
"NEU ERINNERN" to TitleID.ALERT_TO_SNOOZE,
"BESTÄTIGEN" to TitleID.ALERT_TO_CONFIRM,
// Slovenian
"QUICK INFO" to TitleID.QUICK_INFO,
"ODSTOTEK ZBO" to TitleID.TBR_PERCENTAGE,
"TRAJANJE ZBO" to TitleID.TBR_DURATION,
"URA" to TitleID.HOUR,
"MINUTE" to TitleID.MINUTE,
"LETO" to TitleID.YEAR,
"MESEC" to TitleID.MONTH,
"DAN" to TitleID.DAY,
"PODATKI O BOLUSU" to TitleID.BOLUS_DATA,
"PODATKI O NAPAKI" to TitleID.ERROR_DATA,
"DNEVNA PORABA" to TitleID.DAILY_TOTALS,
"PODATKI O ZBO" to TitleID.TBR_DATA,
"UTIŠANJE" to TitleID.ALERT_TO_SNOOZE,
"POTRDITEV" to TitleID.ALERT_TO_CONFIRM,
// Lithuanian
"QUICK INFO" to TitleID.QUICK_INFO,
"TBR REIKŠMĖS" to TitleID.TBR_PERCENTAGE,
"TBR TRUKMĖ" to TitleID.TBR_DURATION,
"VALANDA" to TitleID.HOUR,
"MINUTĖ" to TitleID.MINUTE,
"METAI" to TitleID.YEAR,
"MĖNUO" to TitleID.MONTH,
"DIENA" to TitleID.DAY,
"BOLIUSO DUOMENYS" to TitleID.BOLUS_DATA,
"KLAIDOS DUOMENYS" to TitleID.ERROR_DATA,
"BENDR. DIENOS K." to TitleID.DAILY_TOTALS,
"TBR DUOMENYS" to TitleID.TBR_DATA,
"NUTILDYTI" to TitleID.ALERT_TO_SNOOZE,
"PATVIRTINTI" to TitleID.ALERT_TO_CONFIRM,
// Some pumps came preconfigured with a different quick info name
"ACCU CHECK SPIRIT" to TitleID.QUICK_INFO

View file

@ -13,7 +13,7 @@ import info.nightscout.comboctl.parser.ParsedScreen
import info.nightscout.comboctl.parser.testFrameMainScreenWithTimeSeparator
import info.nightscout.comboctl.parser.testFrameMainScreenWithoutTimeSeparator
import info.nightscout.comboctl.parser.testFrameStandardBolusMenuScreen
import info.nightscout.comboctl.parser.testFrameTbrDurationEnglishScreen
import info.nightscout.comboctl.parser.TbrPercentageAndDurationScreens
import info.nightscout.comboctl.parser.testFrameTemporaryBasalRateNoPercentageScreen
import info.nightscout.comboctl.parser.testFrameTemporaryBasalRatePercentage110Screen
import info.nightscout.comboctl.parser.testFrameW6CancelTbrWarningScreen
@ -300,7 +300,7 @@ class ParsedDisplayFrameStreamTest {
// We expect normal parsing behavior.
stream.feedDisplayFrame(testFrameW6CancelTbrWarningScreen)
val parsedWarningFrame = stream.getParsedDisplayFrame(processAlertScreens = false)
assertEquals(ParsedScreen.AlertScreen(AlertScreenContent.Warning(6)), parsedWarningFrame!!.parsedScreen)
assertEquals(ParsedScreen.AlertScreen(AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)), parsedWarningFrame!!.parsedScreen)
// Feed a W6 screen, but with alert screen detection enabled.
// We expect the alert screen to be detected and an exception
@ -328,7 +328,7 @@ class ParsedDisplayFrameStreamTest {
val displayFrameList = listOf(
testFrameTemporaryBasalRatePercentage110Screen,
testFrameTemporaryBasalRateNoPercentageScreen,
testFrameTbrDurationEnglishScreen
TbrPercentageAndDurationScreens.testFrameTbrDurationEnglishScreen
)
val parsedFrameList = mutableListOf<ParsedDisplayFrame>()

View file

@ -723,7 +723,7 @@ class RTNavigationTest {
)),
ParsedScreen.BasalRate1ProgrammingMenuScreen,
ParsedScreen.BasalRate2ProgrammingMenuScreen,
ParsedScreen.AlertScreen(AlertScreenContent.Warning(code = 6)),
ParsedScreen.AlertScreen(AlertScreenContent.Warning(code = 6, AlertScreenContent.AlertScreenState.TO_SNOOZE)),
ParsedScreen.BasalRate3ProgrammingMenuScreen,
ParsedScreen.BasalRate4ProgrammingMenuScreen,
ParsedScreen.BasalRate5ProgrammingMenuScreen
@ -864,7 +864,7 @@ class RTNavigationTest {
ParsedScreen.TemporaryBasalRatePercentageScreen(170, remainingDurationInMinutes = 30),
ParsedScreen.TemporaryBasalRatePercentageScreen(170, remainingDurationInMinutes = 30),
ParsedScreen.TemporaryBasalRatePercentageScreen(170, remainingDurationInMinutes = 30),
ParsedScreen.AlertScreen(AlertScreenContent.Warning(code = 6)),
ParsedScreen.AlertScreen(AlertScreenContent.Warning(code = 6, AlertScreenContent.AlertScreenState.TO_SNOOZE)),
ParsedScreen.TemporaryBasalRatePercentageScreen(160, remainingDurationInMinutes = 30),
ParsedScreen.TemporaryBasalRatePercentageScreen(160, remainingDurationInMinutes = 30)
))

View file

@ -679,16 +679,16 @@ class ParserTest {
assertEquals(ParseResult.Value::class, result::class)
val alertScreen = (result as ParseResult.Value<*>).value as ParsedScreen.AlertScreen
assertEquals(AlertScreenContent.Warning(6), alertScreen.content)
assertEquals(AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE), alertScreen.content)
}
@Test
fun checkW8CancelBolusWarningScreenParsing() {
val testScreens = listOf(
Pair(testFrameW8CancelBolusWarningScreen0, AlertScreenContent.None),
Pair(testFrameW8CancelBolusWarningScreen1, AlertScreenContent.Warning(8)),
Pair(testFrameW8CancelBolusWarningScreen1, AlertScreenContent.Warning(8, AlertScreenContent.AlertScreenState.TO_SNOOZE)),
Pair(testFrameW8CancelBolusWarningScreen2, AlertScreenContent.None),
Pair(testFrameW8CancelBolusWarningScreen3, AlertScreenContent.Warning(8))
Pair(testFrameW8CancelBolusWarningScreen3, AlertScreenContent.Warning(8, AlertScreenContent.AlertScreenState.TO_CONFIRM))
)
for (testScreen in testScreens) {
@ -704,7 +704,25 @@ class ParserTest {
fun checkE2BatteryEmptyErrorScreenParsing() {
val testScreens = listOf(
Pair(testFrameE2BatteryEmptyErrorScreen0, AlertScreenContent.None),
Pair(testFrameE2BatteryEmptyErrorScreen1, AlertScreenContent.Error(2))
Pair(testFrameE2BatteryEmptyErrorScreen1, AlertScreenContent.Error(2, AlertScreenContent.AlertScreenState.ERROR_TEXT))
)
for (testScreen in testScreens) {
val testContext = TestContext(testScreen.first, 0, skipTitleString = true)
val result = AlertScreenParser().parse(testContext.parseContext)
assertEquals(ParseResult.Value::class, result::class)
val screen = (result as ParseResult.Value<*>).value as ParsedScreen.AlertScreen
assertEquals(testScreen.second, screen.content)
}
}
@Test
fun checkE4OcclusionErrorScreenParsing() {
val testScreens = listOf(
Pair(testFrameE4OcclusionErrorScreen0, AlertScreenContent.Error(4, AlertScreenContent.AlertScreenState.ERROR_TEXT)),
Pair(testFrameE4OcclusionErrorScreen1, AlertScreenContent.None),
Pair(testFrameE4OcclusionErrorScreen2, AlertScreenContent.Error(4, AlertScreenContent.AlertScreenState.ERROR_TEXT)),
Pair(testFrameE4OcclusionErrorScreen3, AlertScreenContent.None),
)
for (testScreen in testScreens) {
@ -722,26 +740,28 @@ class ParserTest {
Pair(testFrameTemporaryBasalRatePercentage100Screen, 100),
Pair(testFrameTemporaryBasalRatePercentage110Screen, 110),
Pair(testFrameTemporaryBasalRateNoPercentageScreen, null),
Pair(testFrameTbrPercentageEnglishScreen, 110),
Pair(testFrameTbrPercentageSpanishScreen, 110),
Pair(testFrameTbrPercentageFrenchScreen, 110),
Pair(testFrameTbrPercentageItalianScreen, 110),
Pair(testFrameTbrPercentageRussianScreen, 110),
Pair(testFrameTbrPercentageTurkishScreen, 110),
Pair(testFrameTbrPercentagePolishScreen, 100),
Pair(testFrameTbrPercentageCzechScreen, 110),
Pair(testFrameTbrPercentageHungarianScreen, 110),
Pair(testFrameTbrPercentageSlovakScreen, 110),
Pair(testFrameTbrPercentageRomanianScreen, 110),
Pair(testFrameTbrPercentageCroatianScreen, 110),
Pair(testFrameTbrPercentageDutchScreen, 110),
Pair(testFrameTbrPercentageGreekScreen, 110),
Pair(testFrameTbrPercentageFinnishScreen, 110),
Pair(testFrameTbrPercentageNorwegianScreen, 110),
Pair(testFrameTbrPercentagePortugueseScreen, 110),
Pair(testFrameTbrPercentageSwedishScreen, 110),
Pair(testFrameTbrPercentageDanishScreen, 110),
Pair(testFrameTbrPercentageGermanScreen, 110)
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageEnglishScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSpanishScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageFrenchScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageItalianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageRussianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageTurkishScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentagePolishScreen, 100),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageCzechScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageHungarianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSlovakScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageRomanianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageCroatianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageDutchScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageGreekScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageFinnishScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageNorwegianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentagePortugueseScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSwedishScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageDanishScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageGermanScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSlovenianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageLithuanianScreen, 110),
)
for (testScreen in testScreens) {
@ -769,26 +789,28 @@ class ParserTest {
fun checkTemporaryBasalRateDurationScreenParsing() {
val testScreens = listOf(
Pair(testFrameTbrDurationNoDurationScreen, null),
Pair(testFrameTbrDurationEnglishScreen, 30),
Pair(testFrameTbrDurationSpanishScreen, 30),
Pair(testFrameTbrDurationFrenchScreen, 30),
Pair(testFrameTbrDurationItalianScreen, 30),
Pair(testFrameTbrDurationRussianScreen, 30),
Pair(testFrameTbrDurationTurkishScreen, 30),
Pair(testFrameTbrDurationPolishScreen, 30),
Pair(testFrameTbrDurationCzechScreen, 30),
Pair(testFrameTbrDurationHungarianScreen, 30),
Pair(testFrameTbrDurationSlovakScreen, 30),
Pair(testFrameTbrDurationRomanianScreen, 30),
Pair(testFrameTbrDurationCroatianScreen, 30),
Pair(testFrameTbrDurationDutchScreen, 30),
Pair(testFrameTbrDurationGreekScreen, 30),
Pair(testFrameTbrDurationFinnishScreen, 30),
Pair(testFrameTbrDurationNorwegianScreen, 30),
Pair(testFrameTbrDurationPortugueseScreen, 30),
Pair(testFrameTbrDurationSwedishScreen, 30),
Pair(testFrameTbrDurationDanishScreen, 30),
Pair(testFrameTbrDurationGermanScreen, 30)
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationEnglishScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSpanishScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationFrenchScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationItalianScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationRussianScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationTurkishScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationPolishScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationCzechScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationHungarianScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSlovakScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationRomanianScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationCroatianScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationDutchScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationGreekScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationFinnishScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationNorwegianScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationPortugueseScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSwedishScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationDanishScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationGermanScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSlovenianScreen, 15),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationLithuanianScreen, 15),
)
for (testScreen in testScreens) {
@ -927,6 +949,18 @@ class ParserTest {
Pair(testTimeAndDateSettingsMonthGermanScreen, ParsedScreen.TimeAndDateSettingsMonthScreen(4)),
Pair(testTimeAndDateSettingsDayGermanScreen, ParsedScreen.TimeAndDateSettingsDayScreen(21)),
Pair(testTimeAndDateSettingsHourSlovenianScreen, ParsedScreen.TimeAndDateSettingsHourScreen(19)),
Pair(testTimeAndDateSettingsMinuteSlovenianScreen, ParsedScreen.TimeAndDateSettingsMinuteScreen(50)),
Pair(testTimeAndDateSettingsYearSlovenianScreen, ParsedScreen.TimeAndDateSettingsYearScreen(2023)),
Pair(testTimeAndDateSettingsMonthSlovenianScreen, ParsedScreen.TimeAndDateSettingsMonthScreen(3)),
Pair(testTimeAndDateSettingsDaySlovenianScreen, ParsedScreen.TimeAndDateSettingsDayScreen(8)),
Pair(testTimeAndDateSettingsHourLithuanianScreen, ParsedScreen.TimeAndDateSettingsHourScreen(20)),
Pair(testTimeAndDateSettingsMinuteLithuanianScreen, ParsedScreen.TimeAndDateSettingsMinuteScreen(16)),
Pair(testTimeAndDateSettingsYearLithuanianScreen, ParsedScreen.TimeAndDateSettingsYearScreen(2023)),
Pair(testTimeAndDateSettingsMonthLithuanianScreen, ParsedScreen.TimeAndDateSettingsMonthScreen(3)),
Pair(testTimeAndDateSettingsDayLithuanianScreen, ParsedScreen.TimeAndDateSettingsDayScreen(8)),
// Extra test to verify that a *minute* 24 is not incorrectly interpreted
// as an *hour* 24 and thus translated to 0 (this is done because the Combo
// may show midnight as both hour 0 and hour 24).
@ -963,7 +997,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0),
alert = AlertScreenContent.Warning(6)
alert = AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -994,7 +1028,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0),
alert = AlertScreenContent.Warning(6)
alert = AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1025,7 +1059,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1056,7 +1090,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1087,7 +1121,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1118,7 +1152,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1149,7 +1183,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1180,7 +1214,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1211,7 +1245,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1242,7 +1276,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1273,7 +1307,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0),
alert = AlertScreenContent.Warning(7)
alert = AlertScreenContent.Warning(7, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1304,7 +1338,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1335,7 +1369,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1366,7 +1400,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1397,7 +1431,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1428,7 +1462,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1459,7 +1493,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1490,7 +1524,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1521,7 +1555,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0),
alert = AlertScreenContent.Warning(1)
alert = AlertScreenContent.Warning(1, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1552,7 +1586,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0),
alert = AlertScreenContent.Warning(6)
alert = AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
@ -1569,7 +1603,69 @@ class ParserTest {
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0),
percentage = 110, durationInMinutes = 0
)
),
Pair(
testMyDataBolusDataSlovenianScreen,
ParsedScreen.MyDataBolusDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 1, hour = 23, minute = 21, second = 0),
bolusAmount = 3200, bolusType = MyDataBolusType.MULTI_WAVE, durationInMinutes = 43
)
),
Pair(
testMyDataErrorDataSlovenianScreen,
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 3, dayOfMonth = 8, hour = 17, minute = 31, second = 0),
alert = AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
testMyDataDailyTotalsSlovenianScreen,
ParsedScreen.MyDataDailyTotalsScreen(
index = 1, totalNumEntries = 30, date = LocalDate(year = 0, dayOfMonth = 8, monthNumber = 3),
totalDailyAmount = 32100
)
),
Pair(
testMyDataTbrDataSlovenianScreen,
ParsedScreen.MyDataTbrDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 3, dayOfMonth = 8, hour = 17, minute = 31, second = 0),
percentage = 110, durationInMinutes = 1
)
),
Pair(
testMyDataBolusDataLithuanianScreen,
ParsedScreen.MyDataBolusDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 1, hour = 23, minute = 21, second = 0),
bolusAmount = 3200, bolusType = MyDataBolusType.MULTI_WAVE, durationInMinutes = 43
)
),
Pair(
testMyDataErrorDataLithuanianScreen,
ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 3, dayOfMonth = 8, hour = 20, minute = 6, second = 0),
alert = AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.HISTORY_ENTRY)
)
),
Pair(
testMyDataDailyTotalsLithuanianScreen,
ParsedScreen.MyDataDailyTotalsScreen(
index = 1, totalNumEntries = 30, date = LocalDate(year = 0, dayOfMonth = 8, monthNumber = 3),
totalDailyAmount = 33600
)
),
Pair(
testMyDataTbrDataLithuanianScreen,
ParsedScreen.MyDataTbrDataScreen(
index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 3, dayOfMonth = 8, hour = 20, minute = 6, second = 0),
percentage = 110, durationInMinutes = 1
)
),
)
for (testScreen in testScreens) {
@ -1595,6 +1691,196 @@ class ParserTest {
}
}
@Test
fun checkAlertScreenTextParsing() {
val testScreens = listOf(
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextEnglishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextEnglishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextSpanishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextSpanishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextFrenchScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextFrenchScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextItalianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextItalianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextRussianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextRussianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextTurkishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextTurkishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextPolishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextPolishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextCzechScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextCzechScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextHungarianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextHungarianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextSlovakScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextSlovakScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextRomanianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextRomanianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextCroatianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextCroatianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextDutchScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextDutchScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextGreekScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextGreekScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextFinnishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextFinnishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextNorwegianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextNorwegianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextPortugueseScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextPortugueseScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextSwedishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextSwedishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextDanishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextDanishScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextGermanScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextGermanScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextSlovenianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextSlovenianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenSnoozeTextLithuanianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_SNOOZE)
),
Pair(
AlertSnoozeAndConfirmScreens.testAlertScreenConfirmTextLithuanianScreen,
AlertScreenContent.Warning(6, AlertScreenContent.AlertScreenState.TO_CONFIRM)
),
)
for (testScreen in testScreens) {
val testContext = TestContext(testScreen.first, 0, skipTitleString = true)
val result = AlertScreenParser().parse(testContext.parseContext)
assertEquals(ParseResult.Value::class, result::class)
val alertScreen = (result as ParseResult.Value<*>).value as ParsedScreen.AlertScreen
assertEquals(testScreen.second, alertScreen.content)
}
}
@Test
fun checkToplevelScreenParsing() {
val testScreens = listOf(

View file

@ -5,6 +5,7 @@ import info.nightscout.comboctl.base.CurrentTbrState
import info.nightscout.comboctl.base.InvariantPumpData
import info.nightscout.comboctl.base.Nonce
import info.nightscout.comboctl.base.PumpStateStore
import info.nightscout.comboctl.base.PumpStateStoreAccessException
import info.nightscout.comboctl.base.Tbr
import info.nightscout.comboctl.base.toBluetoothAddress
import info.nightscout.comboctl.base.toCipher
@ -151,7 +152,7 @@ class AAPSPumpStateStore(
timestamp = Instant.fromEpochSeconds(tbrTimestamp),
percentage = tbrPercentage,
durationInMinutes = tbrDuration,
type = Tbr.Type.fromStringId(tbrType)!!
type = Tbr.Type.fromStringId(tbrType) ?: throw PumpStateStoreAccessException(pumpAddress, "Invalid type \"$tbrType\"")
))
else
CurrentTbrState.NoTbrOngoing

View file

@ -94,8 +94,13 @@ class ComboV2Fragment : DaggerFragment() {
binding.combov2DriverState.text = text
binding.combov2RefreshButton.isEnabled = when (connectionState) {
// Enable the refresh button if:
// 1. Pump is not connected (to be able to manually initiate a pump status update)
// 2. Pump is suspended (in case the user resumed the pump and wants to update the status in AAPS)
// 3. When an error happened (to manually clear the pumpErrorObserved flag and unlock the loop after dealing with the error)
ComboV2Plugin.DriverState.Disconnected,
ComboV2Plugin.DriverState.Suspended -> true
ComboV2Plugin.DriverState.Suspended,
ComboV2Plugin.DriverState.Error-> true
else -> false
}

View file

@ -12,6 +12,7 @@ import dagger.android.HasAndroidInjector
import info.nightscout.comboctl.android.AndroidBluetoothInterface
import info.nightscout.comboctl.base.BasicProgressStage
import info.nightscout.comboctl.base.BluetoothException
import info.nightscout.comboctl.base.BluetoothNotEnabledException
import info.nightscout.comboctl.base.ComboException
import info.nightscout.comboctl.base.DisplayFrame
import info.nightscout.comboctl.base.NullDisplayFrame
@ -65,7 +66,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
@ -134,8 +134,8 @@ class ComboV2Plugin @Inject constructor (
// Coroutine scope and the associated job. All coroutines
// that are started in this plugin are part of this scope.
private val pumpCoroutineMainJob = SupervisorJob()
private val pumpCoroutineScope = CoroutineScope(Dispatchers.Default + pumpCoroutineMainJob)
private var pumpCoroutineScopeJob = SupervisorJob()
private var pumpCoroutineScope = CoroutineScope(Dispatchers.Default + pumpCoroutineScopeJob)
private val _pumpDescription = PumpDescription()
@ -255,6 +255,8 @@ class ComboV2Plugin @Inject constructor (
}
override fun onStart() {
aapsLogger.info(LTag.PUMP, "Starting combov2 driver")
super.onStart()
updateComboCtlLogLevel()
@ -288,23 +290,31 @@ class ComboV2Plugin @Inject constructor (
}
aapsLogger.debug(LTag.PUMP, "Creating bluetooth interface")
bluetoothInterface = AndroidBluetoothInterface(context)
val newBluetoothInterface = AndroidBluetoothInterface(context)
bluetoothInterface = newBluetoothInterface
aapsLogger.info(LTag.PUMP, "Continuing combov2 driver start in coroutine")
// 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 permissions are granted, or the
// coroutine is cancelled (see onStop() below).
pumpCoroutineScope.launch {
try {
runWithPermissionCheck(
context, config, aapsLogger, androidPermission,
permissionsToCheckFor = listOf("android.permission.BLUETOOTH_CONNECT")
) {
aapsLogger.debug(LTag.PUMP, "Setting up bluetooth interface")
bluetoothInterface!!.setup()
try {
newBluetoothInterface.setup()
rxBus.send(EventDismissNotification(Notification.BLUETOOTH_NOT_ENABLED))
aapsLogger.debug(LTag.PUMP, "Setting up pump manager")
pumpManager = ComboCtlPumpManager(bluetoothInterface!!, pumpStateStore)
pumpManager!!.setup {
val newPumpManager = ComboCtlPumpManager(newBluetoothInterface, pumpStateStore)
newPumpManager.setup {
_pairedStateUIFlow.value = false
unpairing = false
}
@ -316,23 +326,50 @@ class ComboV2Plugin @Inject constructor (
// used as the backing store for the isPaired() function,
// so setting up that UI state flow equals updating that
// paired state.
val paired = pumpManager!!.getPairedPumpAddresses().isNotEmpty()
val paired = newPumpManager.getPairedPumpAddresses().isNotEmpty()
_pairedStateUIFlow.value = paired
pumpManager = newPumpManager
} catch (_: BluetoothNotEnabledException) {
uiInteraction.addNotification(
Notification.BLUETOOTH_NOT_ENABLED,
text = rh.gs(info.nightscout.core.ui.R.string.ble_not_enabled),
level = Notification.INFO
)
// If the user currently has Bluetooth disabled, retry until
// the user turns it on. AAPS will automatically show a dialog
// box which requests the user to enable Bluetooth. Upon
// catching this exception, runWithPermissionCheck() will wait
// a bit before retrying, so no delay() call is needed here.
throw RetryPermissionCheckException()
}
setDriverState(DriverState.Disconnected)
aapsLogger.info(LTag.PUMP, "combov2 driver start complete")
// NOTE: EventInitializationChanged is sent in getPumpStatus() .
}
} catch (e: CancellationException) {
aapsLogger.info(LTag.PUMP, "combov2 driver start cancelled")
throw e
}
}
}
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()
aapsLogger.info(LTag.PUMP, "Stopping combov2 driver")
runBlocking {
// Cancel any ongoing background coroutines. This includes an ongoing
// unfinished initialization that still waits for the user to grant
// Bluetooth permissions. Also join to wait for the coroutines to
// finish. Otherwise, race conditions can occur, for example, when
// a coroutine tries to access bluetoothInterface right after it
// was torn down below.
pumpCoroutineScopeJob.cancelAndJoin()
// Normally this should not happen, but to be safe,
// make sure any running pump instance is disconnected.
pump?.disconnect()
@ -353,7 +390,13 @@ class ComboV2Plugin @Inject constructor (
rxBus.send(EventInitializationChanged())
initializationChangedEventSent = false
// The old job and scope were completed. We need new ones.
pumpCoroutineScopeJob = SupervisorJob()
pumpCoroutineScope = CoroutineScope(Dispatchers.Default + pumpCoroutineScopeJob)
super.onStop()
aapsLogger.info(LTag.PUMP, "combov2 driver stopped")
}
override fun preprocessPreferences(preferenceFragment: PreferenceFragmentCompat) {
@ -509,18 +552,18 @@ class ComboV2Plugin @Inject constructor (
}
try {
runBlocking {
pump = pumpManager?.acquirePump(bluetoothAddress, activeBasalProfile) { event -> handlePumpEvent(event) }
val curPumpManager = pumpManager ?: throw Error("Could not get pump manager; this should not happen. Please report this as a bug.")
val acquiredPump = runBlocking {
curPumpManager.acquirePump(bluetoothAddress, activeBasalProfile) { event -> handlePumpEvent(event) }
}
if (pump == null) {
aapsLogger.error(LTag.PUMP, "Could not get pump instance - pump state store may be corrupted")
unpairDueToPumpDataError()
return
}
pump = acquiredPump
_bluetoothAddressUIFlow.value = bluetoothAddress.toString()
_serialNumberUIFlow.value = pumpManager!!.getPumpID(bluetoothAddress)
_serialNumberUIFlow.value = curPumpManager.getPumpID(bluetoothAddress)
rxBus.send(EventDismissNotification(Notification.BLUETOOTH_NOT_ENABLED))
// Erase any display frame that may be left over from a previous connection.
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@ -528,7 +571,7 @@ class ComboV2Plugin @Inject constructor (
stateAndStatusFlowsDeferred = pumpCoroutineScope.async {
coroutineScope {
pump!!.stateFlow
acquiredPump.stateFlow
.onEach { pumpState ->
val driverState = when (pumpState) {
// The Disconnected pump state is ignored, since the Disconnected
@ -549,7 +592,7 @@ class ComboV2Plugin @Inject constructor (
setDriverState(driverState)
}
.launchIn(this)
pump!!.statusFlow
acquiredPump.statusFlow
.onEach { newPumpStatus ->
if (newPumpStatus == null)
return@onEach
@ -571,7 +614,7 @@ class ComboV2Plugin @Inject constructor (
rxBus.send(EventRefreshOverview("ComboV2 pump status updated"))
}
.launchIn(this)
pump!!.lastBolusFlow
acquiredPump.lastBolusFlow
.onEach { lastBolus ->
if (lastBolus == null)
return@onEach
@ -579,7 +622,7 @@ class ComboV2Plugin @Inject constructor (
_lastBolusUIFlow.value = lastBolus
}
.launchIn(this)
pump!!.currentTbrFlow
acquiredPump.currentTbrFlow
.onEach { currentTbr ->
_currentTbrUIFlow.value = currentTbr
}
@ -587,7 +630,7 @@ class ComboV2Plugin @Inject constructor (
}
}
setupUiFlows()
setupUiFlows(acquiredPump)
////
// The actual connect procedure begins here.
@ -696,6 +739,12 @@ class ComboV2Plugin @Inject constructor (
executePendingDisconnect()
}
}
} catch (_: BluetoothNotEnabledException) {
uiInteraction.addNotification(
Notification.BLUETOOTH_NOT_ENABLED,
text = rh.gs(info.nightscout.core.ui.R.string.ble_not_enabled),
level = Notification.INFO
)
} catch (e: Exception) {
aapsLogger.error(LTag.PUMP, "Connection failure: $e")
ToastUtils.showToastInUiThread(context, rh.gs(R.string.combov2_could_not_connect))
@ -766,6 +815,8 @@ class ComboV2Plugin @Inject constructor (
}
}
val acquiredPump = getAcquiredPump()
rxBus.send(EventDismissNotification(Notification.PROFILE_NOT_SET_NOT_INITIALIZED))
rxBus.send(EventDismissNotification(Notification.FAILED_UPDATE_PROFILE))
@ -777,7 +828,7 @@ class ComboV2Plugin @Inject constructor (
runBlocking {
try {
executeCommand {
if (pump!!.setBasalProfile(requestedBasalProfile)) {
if (acquiredPump.setBasalProfile(requestedBasalProfile)) {
aapsLogger.debug(LTag.PUMP, "Basal profiles are different; new profile set")
activeBasalProfile = requestedBasalProfile
updateBaseBasalRateUI()
@ -872,7 +923,6 @@ class ComboV2Plugin @Inject constructor (
pumpSync.insertTherapyEventIfNewWithTimestamp(
timestamp = System.currentTimeMillis(),
type = DetailedBolusInfo.EventType.INSULIN_CHANGE,
note = rh.gs(R.string.combov2_note_reservoir_change),
pumpId = null,
pumpType = PumpType.ACCU_CHEK_COMBO,
pumpSerial = serialNumber()
@ -897,7 +947,6 @@ class ComboV2Plugin @Inject constructor (
pumpSync.insertTherapyEventIfNewWithTimestamp(
timestamp = System.currentTimeMillis(),
type = DetailedBolusInfo.EventType.PUMP_BATTERY_CHANGE,
note = rh.gs(R.string.combov2_note_battery_change),
pumpId = null,
pumpType = PumpType.ACCU_CHEK_COMBO,
pumpSerial = serialNumber()
@ -927,6 +976,8 @@ class ComboV2Plugin @Inject constructor (
// (Also, a zero insulin value makes no sense when bolusing.)
require((detailedBolusInfo.insulin > 0) && (detailedBolusInfo.carbs <= 0.0)) { detailedBolusInfo.toString() }
val acquiredPump = getAcquiredPump()
val requestedBolusAmount = detailedBolusInfo.insulin.iuToCctlBolus()
val bolusReason = when (detailedBolusInfo.bolusType) {
DetailedBolusInfo.BolusType.NORMAL -> ComboCtlPump.StandardBolusReason.NORMAL
@ -960,7 +1011,7 @@ class ComboV2Plugin @Inject constructor (
)
val bolusProgressJob = pumpCoroutineScope.launch {
pump!!.bolusDeliveryProgressFlow
acquiredPump.bolusDeliveryProgressFlow
.collect { progressReport ->
when (progressReport.stage) {
is RTCommandProgressStage.DeliveringBolus -> {
@ -983,14 +1034,16 @@ class ComboV2Plugin @Inject constructor (
// Run the delivery in a sub-coroutine to be able
// to cancel it via stopBolusDelivering().
val newBolusJob = pumpCoroutineScope.async {
// Store a local reference to the Pump instance. "pump"
// is set to null in case of an error, because then,
// disconnectInternal() is called (which sets pump to null).
// However, we still need to access the last delivered bolus
// from the pump's lastBolusFlow, even if an error happened.
// Solve this by storing this reference and accessing the
// lastBolusFlow through it.
val acquiredPump = pump!!
// NOTE: Above, we take a local reference to the acquired Pump instance,
// with a check that throws an exception in case the "pump" member is
// null. This local reference is particularly important inside this
// coroutine, because the "pump" member is set to null in case of an
// error or other disconnect reason (see disconnectInternal()). However,
// we still need to access the last delivered bolus inside this coroutine
// from the pump's lastBolusFlow, even if an error happened. Accessing
// it through the "pump" member would then result in an NPE. This is
// solved by instead accessing the lastBolusFlow through the local
// "acquiredPump" reference.
try {
executeCommand {
@ -1170,11 +1223,13 @@ class ComboV2Plugin @Inject constructor (
return
}
val acquiredPump = getAcquiredPump()
runBlocking {
try {
executeCommand {
val tbrComment = when (pump!!.setTbr(percentage, durationInMinutes, tbrType, force100Percent)) {
val tbrComment = when (acquiredPump.setTbr(percentage, durationInMinutes, tbrType, force100Percent)) {
ComboCtlPump.SetTbrOutcome.SET_NORMAL_TBR ->
rh.gs(R.string.combov2_setting_tbr_succeeded)
ComboCtlPump.SetTbrOutcome.SET_EMULATED_100_TBR ->
@ -1327,8 +1382,9 @@ class ComboV2Plugin @Inject constructor (
override fun serialNumber(): String {
val bluetoothAddress = getBluetoothAddress()
return if ((bluetoothAddress != null) && (pumpManager != null))
pumpManager!!.getPumpID(bluetoothAddress)
val curPumpManager = pumpManager
return if ((bluetoothAddress != null) && (curPumpManager != null))
curPumpManager.getPumpID(bluetoothAddress)
else
rh.gs(R.string.combov2_not_paired)
}
@ -1396,6 +1452,7 @@ class ComboV2Plugin @Inject constructor (
override fun loadTDDs(): PumpEnactResult {
val pumpEnactResult = PumpEnactResult(injector)
val acquiredPump = getAcquiredPump()
runBlocking {
try {
@ -1403,7 +1460,7 @@ class ComboV2Plugin @Inject constructor (
val tddMap = mutableMapOf<Long, Int>()
executeCommand {
val tddHistory = pump!!.fetchTDDHistory()
val tddHistory = acquiredPump.fetchTDDHistory()
tddHistory
.filter { it.totalDailyAmount >= 1 }
@ -1544,6 +1601,7 @@ class ComboV2Plugin @Inject constructor (
context, config, aapsLogger, androidPermission,
permissionsToCheckFor = listOf("android.permission.BLUETOOTH_CONNECT")
) {
try {
pumpManager?.pairWithNewPump(discoveryDuration) { newPumpAddress, previousAttemptFailed ->
aapsLogger.info(
LTag.PUMP,
@ -1553,6 +1611,14 @@ class ComboV2Plugin @Inject constructor (
_previousPairingAttemptFailedFlow.value = previousAttemptFailed
newPINChannel.receive()
} ?: throw IllegalStateException("Attempting to access uninitialized pump manager")
} catch (e: BluetoothNotEnabledException) {
// If Bluetooth is turned off during pairing, show a toaster message.
// Notifications on the AAPS overview fragment are not useful here
// because the pairing activity obscures that fragment. So, instead,
// alert the user by showing the notification via the toaster.
ToastUtils.errorToast(context, info.nightscout.core.ui.R.string.ble_not_enabled)
ComboCtlPumpManager.PairingResult.ExceptionDuringPairing(e)
}
}
if (pairingResult !is ComboCtlPumpManager.PairingResult.Success)
@ -1716,11 +1782,11 @@ class ComboV2Plugin @Inject constructor (
/*** Misc private functions ***/
private fun setupUiFlows() {
private fun setupUiFlows(acquiredPump: ComboCtlPump) {
pumpUIFlowsDeferred = pumpCoroutineScope.async {
try {
coroutineScope {
pump!!.connectProgressFlow
acquiredPump.connectProgressFlow
.onEach { progressReport ->
val description = when (val progStage = progressReport.stage) {
is BasicProgressStage.EstablishingBtConnection ->
@ -1738,7 +1804,7 @@ class ComboV2Plugin @Inject constructor (
}
.launchIn(this)
pump!!.setDateTimeProgressFlow
acquiredPump.setDateTimeProgressFlow
.onEach { progressReport ->
val description = when (progressReport.stage) {
RTCommandProgressStage.SettingDateTimeHour,
@ -1755,7 +1821,7 @@ class ComboV2Plugin @Inject constructor (
}
.launchIn(this)
pump!!.getBasalProfileFlow
acquiredPump.getBasalProfileFlow
.onEach { progressReport ->
val description = when (val stage = progressReport.stage) {
is RTCommandProgressStage.GettingBasalProfile ->
@ -1769,7 +1835,7 @@ class ComboV2Plugin @Inject constructor (
}
.launchIn(this)
pump!!.setBasalProfileFlow
acquiredPump.setBasalProfileFlow
.onEach { progressReport ->
val description = when (val stage = progressReport.stage) {
is RTCommandProgressStage.SettingBasalProfile ->
@ -1783,7 +1849,7 @@ class ComboV2Plugin @Inject constructor (
}
.launchIn(this)
pump!!.bolusDeliveryProgressFlow
acquiredPump.bolusDeliveryProgressFlow
.onEach { progressReport ->
val description = when (val stage = progressReport.stage) {
is RTCommandProgressStage.DeliveringBolus ->
@ -1801,7 +1867,7 @@ class ComboV2Plugin @Inject constructor (
}
.launchIn(this)
pump!!.parsedDisplayFrameFlow
acquiredPump.parsedDisplayFrameFlow
.onEach { parsedDisplayFrame ->
_displayFrameUIFlow.emit(
parsedDisplayFrame?.displayFrame ?: NullDisplayFrame
@ -2003,7 +2069,11 @@ class ComboV2Plugin @Inject constructor (
// It makes no sense to reach this location with pump
// being null due to the checks above.
assert(pump != null)
val pumpToDisconnect = pump
if (pumpToDisconnect == null) {
aapsLogger.error(LTag.PUMP, "Current pump is already null")
return
}
// Run these operations in a coroutine to be able to wait
// until the disconnect really completes and the UI flows
@ -2041,17 +2111,17 @@ class ComboV2Plugin @Inject constructor (
// the Pump.disconnect() call shuts down the RFCOMM socket,
// making all send/receive calls fail.
if (pump!!.stateFlow.value == ComboCtlPump.State.Connecting) {
if (pumpToDisconnect.stateFlow.value == ComboCtlPump.State.Connecting) {
// Case #1 from above
aapsLogger.debug(LTag.PUMP, "Cancelling ongoing connect attempt")
connectionSetupJob?.cancel()
pump?.disconnect()
pumpToDisconnect.disconnect()
connectionSetupJob?.join()
} else {
// Case #2 from above
aapsLogger.debug(LTag.PUMP, "Disconnecting Combo (if not disconnected already by a cancelling request)")
connectionSetupJob?.cancelAndJoin()
pump?.disconnect()
pumpToDisconnect.disconnect()
}
aapsLogger.debug(LTag.PUMP, "Combo disconnected; cancelling UI flows coroutine")
@ -2284,6 +2354,8 @@ class ComboV2Plugin @Inject constructor (
private fun getBluetoothAddress(): ComboCtlBluetoothAddress? =
pumpManager?.getPairedPumpAddresses()?.firstOrNull()
private fun getAcquiredPump() = pump ?: throw Error("There is no currently acquired pump; this should not happen. Please report this as a bug.")
private fun isDisconnected() =
when (driverStateFlow.value) {
DriverState.NotInitialized,

View file

@ -3,6 +3,7 @@ package info.nightscout.pump.combov2
import android.content.Context
import android.os.Build
import info.nightscout.comboctl.android.AndroidBluetoothPermissionException
import info.nightscout.comboctl.base.ComboException
import info.nightscout.comboctl.main.BasalProfile
import info.nightscout.comboctl.main.NUM_COMBO_BASAL_PROFILE_FACTORS
import info.nightscout.interfaces.AndroidPermission
@ -32,7 +33,37 @@ fun AAPSProfile.toComboCtlBasalProfile(): BasalProfile {
return BasalProfile(factors)
}
suspend fun <T> runWithPermissionCheck(
internal class RetryPermissionCheckException : ComboException("retry permission check")
// Utility function to perform Android permission checks before running a block.
// If the permissions are not given, wait for a little while, then retry.
// This is needed for AAPS<->combov2 integration, since AAPS can start combov2
// even _before_ the user granted AAPS BLUETOOTH_CONNECT etc. permissions.
//
// permissionsToCheckFor is a collection of permissions strings like
// Manifest.permission.BLUETOOTH_SCAN. The function goes through the collection,
// and checks each and every permission to see if they have all been granted.
// Only if all have been granted will the block be executed.
//
// It is possible that within the block, some additional permission checks
// are performed - in particular, these can be checks for permissions that
// weren't part of the permissionsToCheckFor collection. If such a permission
// is not granted, the block can throw AndroidBluetoothPermissionException.
// That exception also specifies what exact permissions haven't been granted
// (yet). runWithPermissionCheck() then adds these missing permissions to
// permissionsToCheckFor, and tries its permission check again, this time
// with these extra permissions included. That way, a failed permission
// check within the block does not break anything, and instead, these
// permissions too are re-checked by the same logic as the one that looks
// at the initially specified permissions.
//
// Additionally, the block might perform other checks that are not directly
// permissions but related to them. One example is a check to see if the
// Bluetooth adapter is enabled in addition to checking for Bluetooth
// permissions. When such custom checks fail, they can throw
// RetryPermissionCheckException to inform this function that it should
// retry its run, just as if a permission hadn't been granted.
internal suspend fun <T> runWithPermissionCheck(
context: Context,
config: Config,
aapsLogger: AAPSLogger,
@ -53,12 +84,20 @@ suspend fun <T> runWithPermissionCheck(
}
if (notAllPermissionsGranted) {
delay(1000) // Wait a little bit before retrying to avoid 100% CPU usage
// Wait a little bit before retrying to avoid 100% CPU usage.
// It is currently unknown if there is a way to instead get
// some sort of event from Android to inform that the permissions
// have now been granted, so we have to resort to polling,
// at least for now.
delay(1000)
continue
}
}
return block.invoke()
} catch (e: RetryPermissionCheckException) {
// See the above delay() call, which fulfills the exact same purpose.
delay(1000)
} catch (e: AndroidBluetoothPermissionException) {
permissions = permissionsToCheckFor union e.missingPermissions
}