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 { 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, note: String? = "", vararg listValues: ValueWithUnit?)
fun log(action: Action, source: Sources, vararg listValues: ValueWithUnit?) fun log(action: Action, source: Sources, vararg listValues: ValueWithUnit?)
fun log(action: Action, source: Sources, note: String? = "", listValues: List<ValueWithUnit?> = listOf()) 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 EOELOW_PATCH_ALERTS = 79
const val COMBO_PUMP_SUSPENDED = 80 const val COMBO_PUMP_SUSPENDED = 80
const val COMBO_UNKNOWN_TBR = 81 const val COMBO_UNKNOWN_TBR = 81
const val BLUETOOTH_NOT_ENABLED = 82
const val USER_MESSAGE = 1000 const val USER_MESSAGE = 1000

View file

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

View file

@ -273,7 +273,13 @@ class PumpSyncImplementation @Inject constructor(
pumpSerial = pumpSerial 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)) repository.runTransactionForResult(InsertIfNewByTimestampTherapyEventTransaction(therapyEvent))
.doOnError { .doOnError {
aapsLogger.error(LTag.DATABASE, "Error while saving TherapyEvent", it) aapsLogger.error(LTag.DATABASE, "Error while saving TherapyEvent", it)

View file

@ -7,8 +7,10 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import info.nightscout.comboctl.base.BluetoothAddress import info.nightscout.comboctl.base.BluetoothAddress
import info.nightscout.comboctl.base.BluetoothDevice import info.nightscout.comboctl.base.BluetoothDevice
import info.nightscout.comboctl.base.BluetoothNotEnabledException
import info.nightscout.comboctl.base.BluetoothException import info.nightscout.comboctl.base.BluetoothException
import info.nightscout.comboctl.base.BluetoothInterface import info.nightscout.comboctl.base.BluetoothInterface
import info.nightscout.comboctl.base.BluetoothNotAvailableException
import info.nightscout.comboctl.base.LogLevel import info.nightscout.comboctl.base.LogLevel
import info.nightscout.comboctl.base.Logger import info.nightscout.comboctl.base.Logger
import info.nightscout.comboctl.base.toBluetoothAddress import info.nightscout.comboctl.base.toBluetoothAddress
@ -35,7 +37,10 @@ private val logger = Logger.get("AndroidBluetoothInterface")
* instance is an ideal choice. * instance is an ideal choice.
*/ */
class AndroidBluetoothInterface(private val androidContext: Context) : BluetoothInterface { 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 rfcommServerSocket: SystemBluetoothServerSocket? = null
private var discoveryStarted = false private var discoveryStarted = false
private var discoveryBroadcastReceiver: BroadcastReceiver? = null private var discoveryBroadcastReceiver: BroadcastReceiver? = null
@ -96,11 +101,16 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
else @Suppress("DEPRECATION") getParcelableExtra(name) else @Suppress("DEPRECATION") getParcelableExtra(name)
fun setup() { fun setup() {
val bluetoothManager = androidContext.getSystemService(Context.BLUETOOTH_SERVICE) as SystemBluetoothManager val bluetoothManager = androidContext.getSystemService(Context.BLUETOOTH_SERVICE) as? SystemBluetoothManager
bluetoothAdapter = bluetoothManager.adapter _bluetoothAdapter = bluetoothManager?.adapter
checkIfBluetoothEnabledAndAvailable()
val bondedDevices = checkForConnectPermission(androidContext) { 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)" } 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.) // necessary for correct function, just a detail for sake of completeness.)
logger(LogLevel.DEBUG) { "Setting up RFCOMM listener socket" } logger(LogLevel.DEBUG) { "Setting up RFCOMM listener socket" }
rfcommServerSocket = checkForConnectPermission(androidContext) { rfcommServerSocket = checkForConnectPermission(androidContext) {
bluetoothAdapter!!.listenUsingInsecureRfcommWithServiceRecord( bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
sdpServiceName, sdpServiceName,
Constants.sdpSerialPortUUID Constants.sdpSerialPortUUID
) )
@ -203,7 +213,7 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
logger(LogLevel.DEBUG) { "Closing accepted incoming RFCOMM socket" } logger(LogLevel.DEBUG) { "Closing accepted incoming RFCOMM socket" }
try { try {
socket.close() socket.close()
} catch (e: IOException) { } catch (_: IOException) {
} }
} }
} }
@ -274,20 +284,24 @@ class AndroidBluetoothInterface(private val androidContext: Context) : Bluetooth
stopDiscoveryInternal() stopDiscoveryInternal()
} }
override fun getDevice(deviceAddress: BluetoothAddress): BluetoothDevice = override fun getDevice(deviceAddress: BluetoothAddress): BluetoothDevice {
AndroidBluetoothDevice(androidContext, bluetoothAdapter!!, deviceAddress) checkIfBluetoothEnabledAndAvailable()
return AndroidBluetoothDevice(androidContext, bluetoothAdapter, deviceAddress)
}
override fun getAdapterFriendlyName() = override fun getAdapterFriendlyName() =
checkForConnectPermission(androidContext) { bluetoothAdapter!!.name } checkForConnectPermission(androidContext) { bluetoothAdapter.name }
?: throw BluetoothException("Could not get Bluetooth adapter friendly name") ?: throw BluetoothException("Could not get Bluetooth adapter friendly name")
override fun getPairedDeviceAddresses(): Set<BluetoothAddress> = override fun getPairedDeviceAddresses(): Set<BluetoothAddress> {
try { checkIfBluetoothEnabledAndAvailable()
return try {
deviceAddressLock.lock() deviceAddressLock.lock()
pairedDeviceAddresses.filter { pairedDeviceAddress -> deviceFilterCallback(pairedDeviceAddress) }.toSet() pairedDeviceAddresses.filter { pairedDeviceAddress -> deviceFilterCallback(pairedDeviceAddress) }.toSet()
} finally { } finally {
deviceAddressLock.unlock() deviceAddressLock.unlock()
} }
}
private fun stopDiscoveryInternal() { private fun stopDiscoveryInternal() {
// Close the server socket. This frees RFCOMM resources and ends // 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) { private fun onAclConnected(intent: Intent, foundNewPairedDevice: (deviceAddress: BluetoothAddress) -> Unit) {
// Sanity check in case we get this notification for the // Sanity check in case we get this notification for the
// device already and need to avoid duplicate processing. // 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(message: String) : this(message, null)
constructor(cause: Throwable) : this(null, cause) 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. * a Bluetooth subsystem that has been shut down.
* @throws BluetoothPermissionException if discovery fails because * @throws BluetoothPermissionException if discovery fails because
* scanning and connection permissions are missing. * 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 * @throws BluetoothException if discovery fails due to an underlying
* Bluetooth issue. * Bluetooth issue.
*/ */
@ -172,6 +176,10 @@ interface BluetoothInterface {
* *
* @return BluetoothDevice instance for the device with the * @return BluetoothDevice instance for the device with the
* given address * 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 * @throws IllegalStateException if the interface is in a state
* in which accessing devices is not possible, such as * in which accessing devices is not possible, such as
* a Bluetooth subsystem that has been shut down. * a Bluetooth subsystem that has been shut down.
@ -183,6 +191,8 @@ interface BluetoothInterface {
* *
* @throws BluetoothPermissionException if getting the adapter name * @throws BluetoothPermissionException if getting the adapter name
* fails because connection permissions are missing. * 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 * @throws BluetoothException if getting the adapter name fails
* due to an underlying Bluetooth issue. * due to an underlying Bluetooth issue.
*/ */
@ -205,6 +215,11 @@ interface BluetoothInterface {
* round, it is possible that between the [getPairedDeviceAddresses] * round, it is possible that between the [getPairedDeviceAddresses]
* call and the [onDeviceUnpaired] assignment, a device is * call and the [onDeviceUnpaired] assignment, a device is
* unpaired, and thus does not get noticed. * 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> 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 // 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). // (necessary since the screen may change its contents but still be the same screen).
private var rtScreenAlreadyDismissed = false private var rtScreenAlreadyDismissed = false
private var seenAlertAfterDismissingCounter = 0
// Used in handleAlertScreenContent() to check if the current alert // Used in handleAlertScreenContent() to check if the current alert
// screen contains the same alert as the previous one. // screen contains the same alert as the previous one.
private var lastObservedAlertScreenContent: AlertScreenContent? = null private var lastObservedAlertScreenContent: AlertScreenContent? = null
@ -2401,10 +2402,32 @@ class Pump(
// the two button presses, so there is no need to wait // the two button presses, so there is no need to wait
// for the second screen - just press twice right away. // for the second screen - just press twice right away.
if (!rtScreenAlreadyDismissed) { if (!rtScreenAlreadyDismissed) {
logger(LogLevel.DEBUG) { "Dismissing W$warningCode by short-pressing CHECK twice" } val numRequiredButtonPresses = when (alertScreenContent.state) {
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK) AlertScreenContent.AlertScreenState.TO_SNOOZE -> 2
rtNavigationContext.shortPressButton(RTNavigationButton.CHECK) 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 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. * Possible contents of alert (= warning/error) screens.
*/ */
sealed class AlertScreenContent { sealed class AlertScreenContent {
data class Warning(val code: Int) : AlertScreenContent() enum class AlertScreenState {
data class Error(val code: Int) : AlertScreenContent() 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". * "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.LargeSymbol::class)), // warning/error symbol
OptionalParser(SingleGlyphTypeParser(Glyph.LargeCharacter::class)), // "W" or "E" OptionalParser(SingleGlyphTypeParser(Glyph.LargeCharacter::class)), // "W" or "E"
OptionalParser(IntegerParser()), // warning/error number OptionalParser(IntegerParser()), // warning/error number
OptionalParser(SingleGlyphTypeParser(Glyph.LargeSymbol::class)), // stop symbol (only with errors) OptionalParser(SingleGlyphTypeParser(Glyph.LargeSymbol::class)), // stop symbol (shown in suspended state)
SingleGlyphParser(Glyph.SmallSymbol(SmallSymbol.CHECK)) SingleGlyphParser(Glyph.SmallSymbol(SmallSymbol.CHECK)),
StringParser() // snooze / confirm text
) )
).parse(parseContext) ).parse(parseContext)
@ -1151,14 +1161,27 @@ class AlertScreenParser : Parser() {
return when (parseResult.valueAtOrNull<Glyph>(0)) { return when (parseResult.valueAtOrNull<Glyph>(0)) {
Glyph.LargeSymbol(LargeSymbol.WARNING) -> { 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( ParseResult.Value(ParsedScreen.AlertScreen(
AlertScreenContent.Warning(parseResult.valueAt(2)) AlertScreenContent.Warning(parseResult.valueAt(2), alertState)
)) ))
} }
Glyph.LargeSymbol(LargeSymbol.ERROR) -> { Glyph.LargeSymbol(LargeSymbol.ERROR) -> {
ParseResult.Value(ParsedScreen.AlertScreen( 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 { override fun parseImpl(parseContext: ParseContext): ParseResult {
val parseResult = SequenceParser( val parseResult = SequenceParser(
listOf( listOf(
OptionalParser(SingleGlyphParser(Glyph.SmallSymbol(SmallSymbol.PERCENT))),
SingleGlyphParser(Glyph.LargeSymbol(LargeSymbol.BASAL)), SingleGlyphParser(Glyph.LargeSymbol(LargeSymbol.BASAL)),
OptionalParser(IntegerParser()), // TBR percentage OptionalParser(IntegerParser()), // TBR percentage
SingleGlyphParser(Glyph.LargeSymbol(LargeSymbol.PERCENT)), SingleGlyphParser(Glyph.LargeSymbol(LargeSymbol.PERCENT)),
@ -1654,7 +1678,10 @@ class MyDataErrorDataScreenParser : Parser() {
index = index, index = index,
totalNumEntries = totalNumEntries, totalNumEntries = totalNumEntries,
timestamp = timestamp, 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( Glyph.SmallCharacter('ę') to Pattern(arrayOf(
"█████", "█████",
"", "",

View file

@ -19,7 +19,9 @@ enum class TitleID {
BOLUS_DATA, BOLUS_DATA,
ERROR_DATA, ERROR_DATA,
DAILY_TOTALS, 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, "ERROR DATA" to TitleID.ERROR_DATA,
"DAILY TOTALS" to TitleID.DAILY_TOTALS, "DAILY TOTALS" to TitleID.DAILY_TOTALS,
"TBR DATA" to TitleID.TBR_DATA, "TBR DATA" to TitleID.TBR_DATA,
"TO SNOOZE" to TitleID.ALERT_TO_SNOOZE,
"TO CONFIRM" to TitleID.ALERT_TO_CONFIRM,
// Spanish // Spanish
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -60,6 +64,8 @@ val knownScreenTitles = mapOf(
"DATOS DE ERROR" to TitleID.ERROR_DATA, "DATOS DE ERROR" to TitleID.ERROR_DATA,
"TOTALES DIARIOS" to TitleID.DAILY_TOTALS, "TOTALES DIARIOS" to TitleID.DAILY_TOTALS,
"DATOS DE DBT" to TitleID.TBR_DATA, "DATOS DE DBT" to TitleID.TBR_DATA,
"REPETIR SEÑAL" to TitleID.ALERT_TO_SNOOZE,
"CONFIRMAR" to TitleID.ALERT_TO_CONFIRM,
// French // French
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -74,6 +80,8 @@ val knownScreenTitles = mapOf(
"ERREURS" to TitleID.ERROR_DATA, "ERREURS" to TitleID.ERROR_DATA,
"QUANTITÉS JOURN." to TitleID.DAILY_TOTALS, "QUANTITÉS JOURN." to TitleID.DAILY_TOTALS,
"DBT" to TitleID.TBR_DATA, "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 // Italian
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -88,6 +96,8 @@ val knownScreenTitles = mapOf(
"MEMORIA ALLARMI" to TitleID.ERROR_DATA, "MEMORIA ALLARMI" to TitleID.ERROR_DATA,
"TOTALI GIORNATA" to TitleID.DAILY_TOTALS, "TOTALI GIORNATA" to TitleID.DAILY_TOTALS,
"MEMORIA PBT" to TitleID.TBR_DATA, "MEMORIA PBT" to TitleID.TBR_DATA,
"RIPETI ALLARME" to TitleID.ALERT_TO_SNOOZE,
"PER CONFERMARE" to TitleID.ALERT_TO_CONFIRM,
// Russian // Russian
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -102,6 +112,8 @@ val knownScreenTitles = mapOf(
"ДАHHЫE OБ O ИБ." to TitleID.ERROR_DATA, "ДАHHЫE OБ O ИБ." to TitleID.ERROR_DATA,
"CУTOЧHЫE ДOЗЫ" to TitleID.DAILY_TOTALS, "CУTOЧHЫE ДOЗЫ" to TitleID.DAILY_TOTALS,
"ДАHHЫE O BБC" to TitleID.TBR_DATA, "ДА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 // Turkish
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -116,6 +128,8 @@ val knownScreenTitles = mapOf(
"HATA VERİLERİ" to TitleID.ERROR_DATA, "HATA VERİLERİ" to TitleID.ERROR_DATA,
"GÜNLÜK TOPLAM" to TitleID.DAILY_TOTALS, "GÜNLÜK TOPLAM" to TitleID.DAILY_TOTALS,
"GBH VERİLERİ" to TitleID.TBR_DATA, "GBH VERİLERİ" to TitleID.TBR_DATA,
"ERTELE" to TitleID.ALERT_TO_SNOOZE,
"ONAYLA" to TitleID.ALERT_TO_CONFIRM,
// Polish // Polish
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -130,6 +144,8 @@ val knownScreenTitles = mapOf(
"DANE BŁĘDU" to TitleID.ERROR_DATA, "DANE BŁĘDU" to TitleID.ERROR_DATA,
"DZIEN. D. CAŁK." to TitleID.DAILY_TOTALS, "DZIEN. D. CAŁK." to TitleID.DAILY_TOTALS,
"DANE TDP" to TitleID.TBR_DATA, "DANE TDP" to TitleID.TBR_DATA,
"ABY WYCISZYĆ" to TitleID.ALERT_TO_SNOOZE,
"ABY POTWIERDZ." to TitleID.ALERT_TO_CONFIRM,
// Czech // Czech
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -144,6 +160,8 @@ val knownScreenTitles = mapOf(
"ÚDAJE CHYB" to TitleID.ERROR_DATA, "ÚDAJE CHYB" to TitleID.ERROR_DATA,
"CELK. DEN. DÁVKY" to TitleID.DAILY_TOTALS, "CELK. DEN. DÁVKY" to TitleID.DAILY_TOTALS,
"ÚDAJE DBD" to TitleID.TBR_DATA, "ÚDAJE DBD" to TitleID.TBR_DATA,
"ODLOŽIT" to TitleID.ALERT_TO_SNOOZE,
"POTVRDIT" to TitleID.ALERT_TO_CONFIRM,
// Hungarian // Hungarian
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -158,6 +176,8 @@ val knownScreenTitles = mapOf(
"HIBAADATOK" to TitleID.ERROR_DATA, "HIBAADATOK" to TitleID.ERROR_DATA,
"NAPI TELJES" to TitleID.DAILY_TOTALS, "NAPI TELJES" to TitleID.DAILY_TOTALS,
"TBR-ADATOK" to TitleID.TBR_DATA, "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 // Slovak
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -172,6 +192,8 @@ val knownScreenTitles = mapOf(
"DÁTA O CHYBÁCH" to TitleID.ERROR_DATA, "DÁTA O CHYBÁCH" to TitleID.ERROR_DATA,
"SÚČTY DŇA" to TitleID.DAILY_TOTALS, "SÚČTY DŇA" to TitleID.DAILY_TOTALS,
"DBD DÁTA" to TitleID.TBR_DATA, "DBD DÁTA" to TitleID.TBR_DATA,
"STLMI" to TitleID.ALERT_TO_SNOOZE,
"POTVRDI" to TitleID.ALERT_TO_CONFIRM,
// Romanian // Romanian
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -186,6 +208,8 @@ val knownScreenTitles = mapOf(
"DATE EROARE" to TitleID.ERROR_DATA, "DATE EROARE" to TitleID.ERROR_DATA,
"TOTALURI ZILNICE" to TitleID.DAILY_TOTALS, "TOTALURI ZILNICE" to TitleID.DAILY_TOTALS,
"DATE RBT" to TitleID.TBR_DATA, "DATE RBT" to TitleID.TBR_DATA,
"OPRIRE SONERIE" to TitleID.ALERT_TO_SNOOZE,
"CONFIRMARE" to TitleID.ALERT_TO_CONFIRM,
// Croatian // Croatian
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -200,6 +224,8 @@ val knownScreenTitles = mapOf(
"PODACI O GREŠK." to TitleID.ERROR_DATA, "PODACI O GREŠK." to TitleID.ERROR_DATA,
"UKUPNE DNEV.DOZE" to TitleID.DAILY_TOTALS, "UKUPNE DNEV.DOZE" to TitleID.DAILY_TOTALS,
"PODACI O PBD-U" to TitleID.TBR_DATA, "PODACI O PBD-U" to TitleID.TBR_DATA,
"ZA ODGODU" to TitleID.ALERT_TO_SNOOZE,
"ZA POTVRDU" to TitleID.ALERT_TO_CONFIRM,
// Dutch // Dutch
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -214,6 +240,8 @@ val knownScreenTitles = mapOf(
"FOUTENGEGEVENS" to TitleID.ERROR_DATA, "FOUTENGEGEVENS" to TitleID.ERROR_DATA,
"DAGTOTALEN" to TitleID.DAILY_TOTALS, "DAGTOTALEN" to TitleID.DAILY_TOTALS,
"TBD-GEGEVENS" to TitleID.TBR_DATA, "TBD-GEGEVENS" to TitleID.TBR_DATA,
"UITSTELLEN" to TitleID.ALERT_TO_SNOOZE,
"BEVESTIGEN" to TitleID.ALERT_TO_CONFIRM,
// Greek // Greek
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -228,6 +256,8 @@ val knownScreenTitles = mapOf(
"ΔEΔOМ. ΣΦАΛМАTΩN" to TitleID.ERROR_DATA, "ΔEΔOМ. ΣΦАΛМАTΩN" to TitleID.ERROR_DATA,
"HМEPHΣIO ΣΥNOΛO" to TitleID.DAILY_TOTALS, "HМEPHΣIO ΣΥNOΛO" to TitleID.DAILY_TOTALS,
"ΔEΔOМENА П.B.P." to TitleID.TBR_DATA, "Δ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 // Finnish
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -242,6 +272,8 @@ val knownScreenTitles = mapOf(
"HÄLYTYSTIEDOT" to TitleID.ERROR_DATA, "HÄLYTYSTIEDOT" to TitleID.ERROR_DATA,
"PÄIV. KOK.ANNOS" to TitleID.DAILY_TOTALS, "PÄIV. KOK.ANNOS" to TitleID.DAILY_TOTALS,
"TBA - TIEDOT" to TitleID.TBR_DATA, "TBA - TIEDOT" to TitleID.TBR_DATA,
"ILMOITA MYÖH." to TitleID.ALERT_TO_SNOOZE,
"VAHVISTA" to TitleID.ALERT_TO_CONFIRM,
// Norwegian // Norwegian
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -256,6 +288,8 @@ val knownScreenTitles = mapOf(
"FEILDATA" to TitleID.ERROR_DATA, "FEILDATA" to TitleID.ERROR_DATA,
"DØGNMENGDE" to TitleID.DAILY_TOTALS, "DØGNMENGDE" to TitleID.DAILY_TOTALS,
"MBD-DATA" to TitleID.TBR_DATA, "MBD-DATA" to TitleID.TBR_DATA,
"FOR Å SLUMRE" to TitleID.ALERT_TO_SNOOZE,
"FOR Å BEKREFTE" to TitleID.ALERT_TO_CONFIRM,
// Portuguese // Portuguese
"QUICK INFO" to TitleID.QUICK_INFO, "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, "DADOS DE ERROS" to TitleID.ERROR_DATA, "DADOS DE ALARMES" to TitleID.ERROR_DATA,
"TOTAIS DIÁRIOS" to TitleID.DAILY_TOTALS, "TOTAIS DIÁRIOS" to TitleID.DAILY_TOTALS,
"DADOS DBT" to TitleID.TBR_DATA, "DADOS DBT" to TitleID.TBR_DATA,
"PARA SILENCIAR" to TitleID.ALERT_TO_SNOOZE,
"PARA CONFIRMAR" to TitleID.ALERT_TO_CONFIRM,
// Swedish // Swedish
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -285,6 +321,8 @@ val knownScreenTitles = mapOf(
"FELDATA" to TitleID.ERROR_DATA, "FELDATA" to TitleID.ERROR_DATA,
"DYGNSHISTORIK" to TitleID.DAILY_TOTALS, "DYGNSHISTORIK" to TitleID.DAILY_TOTALS,
"TBD DATA" to TitleID.TBR_DATA, "TBD DATA" to TitleID.TBR_DATA,
"SNOOZE" to TitleID.ALERT_TO_SNOOZE,
"BEKRÄFTA" to TitleID.ALERT_TO_CONFIRM,
// Danish // Danish
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -299,6 +337,8 @@ val knownScreenTitles = mapOf(
"FEJLDATA" to TitleID.ERROR_DATA, "FEJLDATA" to TitleID.ERROR_DATA,
"DAGLIG TOTAL" to TitleID.DAILY_TOTALS, "DAGLIG TOTAL" to TitleID.DAILY_TOTALS,
"MBR-DATA" to TitleID.TBR_DATA, "MBR-DATA" to TitleID.TBR_DATA,
"FOR AT UDSÆTTE" to TitleID.ALERT_TO_SNOOZE,
"FOR GODKEND" to TitleID.ALERT_TO_CONFIRM,
// German // German
"QUICK INFO" to TitleID.QUICK_INFO, "QUICK INFO" to TitleID.QUICK_INFO,
@ -313,6 +353,40 @@ val knownScreenTitles = mapOf(
"FEHLERMELDUNGEN" to TitleID.ERROR_DATA, "FEHLERMELDUNGEN" to TitleID.ERROR_DATA,
"TAGESGESAMTMENGE" to TitleID.DAILY_TOTALS, "TAGESGESAMTMENGE" to TitleID.DAILY_TOTALS,
"TBR-INFORMATION" to TitleID.TBR_DATA, "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 // Some pumps came preconfigured with a different quick info name
"ACCU CHECK SPIRIT" to TitleID.QUICK_INFO "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.testFrameMainScreenWithTimeSeparator
import info.nightscout.comboctl.parser.testFrameMainScreenWithoutTimeSeparator import info.nightscout.comboctl.parser.testFrameMainScreenWithoutTimeSeparator
import info.nightscout.comboctl.parser.testFrameStandardBolusMenuScreen 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.testFrameTemporaryBasalRateNoPercentageScreen
import info.nightscout.comboctl.parser.testFrameTemporaryBasalRatePercentage110Screen import info.nightscout.comboctl.parser.testFrameTemporaryBasalRatePercentage110Screen
import info.nightscout.comboctl.parser.testFrameW6CancelTbrWarningScreen import info.nightscout.comboctl.parser.testFrameW6CancelTbrWarningScreen
@ -300,7 +300,7 @@ class ParsedDisplayFrameStreamTest {
// We expect normal parsing behavior. // We expect normal parsing behavior.
stream.feedDisplayFrame(testFrameW6CancelTbrWarningScreen) stream.feedDisplayFrame(testFrameW6CancelTbrWarningScreen)
val parsedWarningFrame = stream.getParsedDisplayFrame(processAlertScreens = false) 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. // Feed a W6 screen, but with alert screen detection enabled.
// We expect the alert screen to be detected and an exception // We expect the alert screen to be detected and an exception
@ -328,7 +328,7 @@ class ParsedDisplayFrameStreamTest {
val displayFrameList = listOf( val displayFrameList = listOf(
testFrameTemporaryBasalRatePercentage110Screen, testFrameTemporaryBasalRatePercentage110Screen,
testFrameTemporaryBasalRateNoPercentageScreen, testFrameTemporaryBasalRateNoPercentageScreen,
testFrameTbrDurationEnglishScreen TbrPercentageAndDurationScreens.testFrameTbrDurationEnglishScreen
) )
val parsedFrameList = mutableListOf<ParsedDisplayFrame>() val parsedFrameList = mutableListOf<ParsedDisplayFrame>()

View file

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

View file

@ -679,16 +679,16 @@ class ParserTest {
assertEquals(ParseResult.Value::class, result::class) assertEquals(ParseResult.Value::class, result::class)
val alertScreen = (result as ParseResult.Value<*>).value as ParsedScreen.AlertScreen 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 @Test
fun checkW8CancelBolusWarningScreenParsing() { fun checkW8CancelBolusWarningScreenParsing() {
val testScreens = listOf( val testScreens = listOf(
Pair(testFrameW8CancelBolusWarningScreen0, AlertScreenContent.None), Pair(testFrameW8CancelBolusWarningScreen0, AlertScreenContent.None),
Pair(testFrameW8CancelBolusWarningScreen1, AlertScreenContent.Warning(8)), Pair(testFrameW8CancelBolusWarningScreen1, AlertScreenContent.Warning(8, AlertScreenContent.AlertScreenState.TO_SNOOZE)),
Pair(testFrameW8CancelBolusWarningScreen2, AlertScreenContent.None), Pair(testFrameW8CancelBolusWarningScreen2, AlertScreenContent.None),
Pair(testFrameW8CancelBolusWarningScreen3, AlertScreenContent.Warning(8)) Pair(testFrameW8CancelBolusWarningScreen3, AlertScreenContent.Warning(8, AlertScreenContent.AlertScreenState.TO_CONFIRM))
) )
for (testScreen in testScreens) { for (testScreen in testScreens) {
@ -704,7 +704,25 @@ class ParserTest {
fun checkE2BatteryEmptyErrorScreenParsing() { fun checkE2BatteryEmptyErrorScreenParsing() {
val testScreens = listOf( val testScreens = listOf(
Pair(testFrameE2BatteryEmptyErrorScreen0, AlertScreenContent.None), 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) { for (testScreen in testScreens) {
@ -722,26 +740,28 @@ class ParserTest {
Pair(testFrameTemporaryBasalRatePercentage100Screen, 100), Pair(testFrameTemporaryBasalRatePercentage100Screen, 100),
Pair(testFrameTemporaryBasalRatePercentage110Screen, 110), Pair(testFrameTemporaryBasalRatePercentage110Screen, 110),
Pair(testFrameTemporaryBasalRateNoPercentageScreen, null), Pair(testFrameTemporaryBasalRateNoPercentageScreen, null),
Pair(testFrameTbrPercentageEnglishScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageEnglishScreen, 110),
Pair(testFrameTbrPercentageSpanishScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSpanishScreen, 110),
Pair(testFrameTbrPercentageFrenchScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageFrenchScreen, 110),
Pair(testFrameTbrPercentageItalianScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageItalianScreen, 110),
Pair(testFrameTbrPercentageRussianScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageRussianScreen, 110),
Pair(testFrameTbrPercentageTurkishScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageTurkishScreen, 110),
Pair(testFrameTbrPercentagePolishScreen, 100), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentagePolishScreen, 100),
Pair(testFrameTbrPercentageCzechScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageCzechScreen, 110),
Pair(testFrameTbrPercentageHungarianScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageHungarianScreen, 110),
Pair(testFrameTbrPercentageSlovakScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSlovakScreen, 110),
Pair(testFrameTbrPercentageRomanianScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageRomanianScreen, 110),
Pair(testFrameTbrPercentageCroatianScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageCroatianScreen, 110),
Pair(testFrameTbrPercentageDutchScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageDutchScreen, 110),
Pair(testFrameTbrPercentageGreekScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageGreekScreen, 110),
Pair(testFrameTbrPercentageFinnishScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageFinnishScreen, 110),
Pair(testFrameTbrPercentageNorwegianScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageNorwegianScreen, 110),
Pair(testFrameTbrPercentagePortugueseScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentagePortugueseScreen, 110),
Pair(testFrameTbrPercentageSwedishScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSwedishScreen, 110),
Pair(testFrameTbrPercentageDanishScreen, 110), Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageDanishScreen, 110),
Pair(testFrameTbrPercentageGermanScreen, 110) Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageGermanScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageSlovenianScreen, 110),
Pair(TbrPercentageAndDurationScreens.testFrameTbrPercentageLithuanianScreen, 110),
) )
for (testScreen in testScreens) { for (testScreen in testScreens) {
@ -769,26 +789,28 @@ class ParserTest {
fun checkTemporaryBasalRateDurationScreenParsing() { fun checkTemporaryBasalRateDurationScreenParsing() {
val testScreens = listOf( val testScreens = listOf(
Pair(testFrameTbrDurationNoDurationScreen, null), Pair(testFrameTbrDurationNoDurationScreen, null),
Pair(testFrameTbrDurationEnglishScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationEnglishScreen, 30),
Pair(testFrameTbrDurationSpanishScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSpanishScreen, 30),
Pair(testFrameTbrDurationFrenchScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationFrenchScreen, 30),
Pair(testFrameTbrDurationItalianScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationItalianScreen, 30),
Pair(testFrameTbrDurationRussianScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationRussianScreen, 30),
Pair(testFrameTbrDurationTurkishScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationTurkishScreen, 30),
Pair(testFrameTbrDurationPolishScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationPolishScreen, 30),
Pair(testFrameTbrDurationCzechScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationCzechScreen, 30),
Pair(testFrameTbrDurationHungarianScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationHungarianScreen, 30),
Pair(testFrameTbrDurationSlovakScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSlovakScreen, 30),
Pair(testFrameTbrDurationRomanianScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationRomanianScreen, 30),
Pair(testFrameTbrDurationCroatianScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationCroatianScreen, 30),
Pair(testFrameTbrDurationDutchScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationDutchScreen, 30),
Pair(testFrameTbrDurationGreekScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationGreekScreen, 30),
Pair(testFrameTbrDurationFinnishScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationFinnishScreen, 30),
Pair(testFrameTbrDurationNorwegianScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationNorwegianScreen, 30),
Pair(testFrameTbrDurationPortugueseScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationPortugueseScreen, 30),
Pair(testFrameTbrDurationSwedishScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSwedishScreen, 30),
Pair(testFrameTbrDurationDanishScreen, 30), Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationDanishScreen, 30),
Pair(testFrameTbrDurationGermanScreen, 30) Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationGermanScreen, 30),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationSlovenianScreen, 15),
Pair(TbrPercentageAndDurationScreens.testFrameTbrDurationLithuanianScreen, 15),
) )
for (testScreen in testScreens) { for (testScreen in testScreens) {
@ -927,6 +949,18 @@ class ParserTest {
Pair(testTimeAndDateSettingsMonthGermanScreen, ParsedScreen.TimeAndDateSettingsMonthScreen(4)), Pair(testTimeAndDateSettingsMonthGermanScreen, ParsedScreen.TimeAndDateSettingsMonthScreen(4)),
Pair(testTimeAndDateSettingsDayGermanScreen, ParsedScreen.TimeAndDateSettingsDayScreen(21)), 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 // 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 // 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). // may show midnight as both hour 0 and hour 24).
@ -963,7 +997,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0), 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( Pair(
@ -994,7 +1028,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0), 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( Pair(
@ -1025,7 +1059,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1056,7 +1090,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1087,7 +1121,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1118,7 +1152,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1149,7 +1183,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1180,7 +1214,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1211,7 +1245,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1242,7 +1276,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1273,7 +1307,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 5, dayOfMonth = 11, hour = 21, minute = 56, second = 0), 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( Pair(
@ -1304,7 +1338,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1335,7 +1369,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1366,7 +1400,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1397,7 +1431,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1428,7 +1462,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1459,7 +1493,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1490,7 +1524,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1521,7 +1555,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 2, dayOfMonth = 1, hour = 1, minute = 6, second = 0), 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( Pair(
@ -1552,7 +1586,7 @@ class ParserTest {
ParsedScreen.MyDataErrorDataScreen( ParsedScreen.MyDataErrorDataScreen(
index = 1, totalNumEntries = 30, index = 1, totalNumEntries = 30,
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0), 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( Pair(
@ -1569,7 +1603,69 @@ class ParserTest {
timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0), timestamp = LocalDateTime(year = 0, monthNumber = 1, dayOfMonth = 28, hour = 11, minute = 0, second = 0),
percentage = 110, durationInMinutes = 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) { 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 @Test
fun checkToplevelScreenParsing() { fun checkToplevelScreenParsing() {
val testScreens = listOf( 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.InvariantPumpData
import info.nightscout.comboctl.base.Nonce import info.nightscout.comboctl.base.Nonce
import info.nightscout.comboctl.base.PumpStateStore import info.nightscout.comboctl.base.PumpStateStore
import info.nightscout.comboctl.base.PumpStateStoreAccessException
import info.nightscout.comboctl.base.Tbr import info.nightscout.comboctl.base.Tbr
import info.nightscout.comboctl.base.toBluetoothAddress import info.nightscout.comboctl.base.toBluetoothAddress
import info.nightscout.comboctl.base.toCipher import info.nightscout.comboctl.base.toCipher
@ -151,7 +152,7 @@ class AAPSPumpStateStore(
timestamp = Instant.fromEpochSeconds(tbrTimestamp), timestamp = Instant.fromEpochSeconds(tbrTimestamp),
percentage = tbrPercentage, percentage = tbrPercentage,
durationInMinutes = tbrDuration, durationInMinutes = tbrDuration,
type = Tbr.Type.fromStringId(tbrType)!! type = Tbr.Type.fromStringId(tbrType) ?: throw PumpStateStoreAccessException(pumpAddress, "Invalid type \"$tbrType\"")
)) ))
else else
CurrentTbrState.NoTbrOngoing CurrentTbrState.NoTbrOngoing

View file

@ -94,8 +94,13 @@ class ComboV2Fragment : DaggerFragment() {
binding.combov2DriverState.text = text binding.combov2DriverState.text = text
binding.combov2RefreshButton.isEnabled = when (connectionState) { 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.Disconnected,
ComboV2Plugin.DriverState.Suspended -> true ComboV2Plugin.DriverState.Suspended,
ComboV2Plugin.DriverState.Error-> true
else -> false else -> false
} }

View file

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

View file

@ -3,6 +3,7 @@ package info.nightscout.pump.combov2
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import info.nightscout.comboctl.android.AndroidBluetoothPermissionException import info.nightscout.comboctl.android.AndroidBluetoothPermissionException
import info.nightscout.comboctl.base.ComboException
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.AndroidPermission
@ -32,7 +33,37 @@ fun AAPSProfile.toComboCtlBasalProfile(): BasalProfile {
return BasalProfile(factors) 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, context: Context,
config: Config, config: Config,
aapsLogger: AAPSLogger, aapsLogger: AAPSLogger,
@ -53,12 +84,20 @@ suspend fun <T> runWithPermissionCheck(
} }
if (notAllPermissionsGranted) { 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 continue
} }
} }
return block.invoke() return block.invoke()
} catch (e: RetryPermissionCheckException) {
// See the above delay() call, which fulfills the exact same purpose.
delay(1000)
} catch (e: AndroidBluetoothPermissionException) { } catch (e: AndroidBluetoothPermissionException) {
permissions = permissionsToCheckFor union e.missingPermissions permissions = permissionsToCheckFor union e.missingPermissions
} }