From ec4280bc2e179f6261ad9980ef1cd0ec3aca83d7 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 19:19:01 +0100 Subject: [PATCH 01/16] Fix creating a seconds-based bolus record based on minute and bolus. (cherry picked from commit f8848be) --- .../androidaps/plugins/PumpCombo/ComboPlugin.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index b3f2c9e570..b3f1aebab7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -602,7 +602,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf private boolean addBolusToTreatments(DetailedBolusInfo detailedBolusInfo, Bolus lastPumpBolus) { DetailedBolusInfo dbi = detailedBolusInfo.copy(); dbi.date = calculateFakeBolusDate(lastPumpBolus); - dbi.pumpId = calculateFakeBolusDate(lastPumpBolus); + dbi.pumpId = dbi.date; dbi.source = Source.PUMP; dbi.insulin = lastPumpBolus.amount; try { @@ -1055,7 +1055,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf for (Bolus pumpBolus : history.bolusHistory) { DetailedBolusInfo dbi = new DetailedBolusInfo(); dbi.date = calculateFakeBolusDate(pumpBolus); - dbi.pumpId = calculateFakeBolusDate(pumpBolus); + dbi.pumpId = dbi.date; dbi.source = Source.PUMP; dbi.insulin = pumpBolus.amount; dbi.eventType = CareportalEvent.CORRECTIONBOLUS; @@ -1067,13 +1067,15 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf } /** Adds the bolus to the timestamp to be able to differentiate multiple boluses in the same - * minute. Best effort, since this covers only boluses up to 5.9 U and relies on other code + * minute. Best effort, since this covers only boluses up to 6.0 U and relies on other code * to prevent a boluses with the same amount to be delivered within the same minute. * Should be good enough, even with command mode, it's a challenge to create that situation * and most time clashes will be around SMBs which are covered. */ private long calculateFakeBolusDate(Bolus pumpBolus) { - return pumpBolus.timestamp + (Math.min((int) (pumpBolus.amount - 0.1) * 10 * 1000, 59 * 1000)); + double bolus = pumpBolus.amount - 0.1; + int secondsFromBolus = (int) (bolus * 10 * 1000); + return pumpBolus.timestamp + Math.min(secondsFromBolus, 59 * 1000); } // TODO use queue once ready From 72e4cd29c4dccd29065df40115d05d25c0af07b1 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 18:01:15 +0100 Subject: [PATCH 02/16] Extend ReadQuickInfoCommand to read more history records. Also remove switch to forego quick info, which really isn't the cause of the infamous bug, but is now required with the updated history check logic against timestamp dups. (cherry picked from commit 18aa827) --- .../plugins/PumpCombo/ComboPlugin.java | 10 ++--- .../ruffyscripter/RuffyCommands.java | 2 +- .../ruffyscripter/RuffyScripter.java | 14 ++----- .../commands/ReadQuickInfoCommand.java | 40 +++++++++++++++++-- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index b3f1aebab7..bf8d91bac1 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -370,7 +370,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf } // trigger a connect, which will update state and check history - CommandResult stateResult = runCommand(null,1, ruffyScripter::readPumpState); + CommandResult stateResult = runCommand(null, 1, ruffyScripter::readPumpState); if (!stateResult.success) { return; } @@ -404,7 +404,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf // ComboFragment updates state fully only after the pump has initialized, // so force an update after initialization completed - updateLocalData(runCommand(null, 1, ruffyScripter::readQuickInfo)); + updateLocalData(runCommand(null, 1, () -> ruffyScripter.readQuickInfo(1))); } /** Updates local cache with state (reservoir level, last bolus ...) returned from the pump */ @@ -542,7 +542,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf // history below to see what was actually delivered // get last bolus from pump history for verification - CommandResult postBolusStateResult = runCommand(null, 3, ruffyScripter::readQuickInfo); + CommandResult postBolusStateResult = runCommand(null, 3, () -> ruffyScripter.readQuickInfo(1)); if (!postBolusStateResult.success) { return new PumpEnactResult().success(false).enacted(false) .comment(MainApp.gs(R.string.combo_error_bolus_verification_failed)); @@ -552,7 +552,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf : null; // no bolus delivered? - if (lastPumpBolus == null || lastPumpBolus.equals(previousBolus) ) { + if (lastPumpBolus == null || lastPumpBolus.equals(previousBolus)) { if (cancelBolus) { return new PumpEnactResult().success(true).enacted(false); } else { @@ -855,7 +855,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf // turn benign warnings into notifications notifyAboutPumpWarning(activeAlert); ruffyScripter.confirmAlert(activeAlert.warningCode); - } else if (activeAlert.errorCode != null){ + } else if (activeAlert.errorCode != null) { Notification notification = new Notification(); notification.date = new Date(); notification.id = Notification.COMBO_PUMP_ALARM; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyCommands.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyCommands.java index 80f57774ce..5918023d1e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyCommands.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyCommands.java @@ -32,7 +32,7 @@ public interface RuffyCommands { CommandResult readPumpState(); /** Read reservoir level and last bolus via Quick Info */ - CommandResult readQuickInfo(); + CommandResult readQuickInfo(int numberOfBolusRecordsToRetrieve); /** Reads pump history via the My Data menu. The {@link PumpHistoryRequest} specifies * what types of data and how far back data is returned. */ diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java index cdbb40c413..4112306fdd 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java @@ -48,8 +48,6 @@ import info.nightscout.androidaps.BuildConfig; * operations and are cleanly separated from the thread management, connection management etc */ public class RuffyScripter implements RuffyCommands { - private final boolean readQuickInfo = true; - private static final Logger log = LoggerFactory.getLogger(RuffyScripter.class); private IRuffyService ruffyService; @@ -225,17 +223,11 @@ public class RuffyScripter implements RuffyCommands { } @Override - public CommandResult readQuickInfo() { - if (readQuickInfo) { - Answers.getInstance().logCustom(new CustomEvent("ComboReadQuickInfoCmd") - .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) - .putCustomAttribute("version", BuildConfig.VERSION)); - return runCommand(new ReadQuickInfoCommand()); - } - Answers.getInstance().logCustom(new CustomEvent("ComboReadHistoryCmd") + public CommandResult readQuickInfo(int numberOfBolusRecordsToRetrieve) { + Answers.getInstance().logCustom(new CustomEvent("ComboReadQuickInfoCmd") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION)); - return runCommand(new ReadHistoryCommand(new PumpHistoryRequest().bolusHistory(PumpHistoryRequest.LAST))); + return runCommand(new ReadQuickInfoCommand(numberOfBolusRecordsToRetrieve)); } public void returnToRootMenu() { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java index b1e80c4e61..2973091c67 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java @@ -2,26 +2,58 @@ package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; import org.monkey.d.ruffy.ruffy.driver.display.MenuAttribute; import org.monkey.d.ruffy.ruffy.driver.display.MenuType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Date; import java.util.List; import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistory; public class ReadQuickInfoCommand extends BaseCommand { + private static final Logger log = LoggerFactory.getLogger(ReadQuickInfoCommand.class); + + private final int numberOfBolusRecordsToRetrieve; + + public ReadQuickInfoCommand(int numberOfBolusRecordsToRetrieve) { + this.numberOfBolusRecordsToRetrieve = numberOfBolusRecordsToRetrieve; + } + @Override public void execute() { scripter.verifyRootMenuIsDisplayed(); + // navigate to reservoir menu scripter.pressCheckKey(); scripter.waitForMenuToBeLeft(MenuType.MAIN_MENU); scripter.waitForMenuToBeLeft(MenuType.STOP); scripter.verifyMenuIsDisplayed(MenuType.QUICK_INFO); result.reservoirLevel = ((Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.REMAINING_INSULIN)).intValue(); - scripter.pressCheckKey(); - List bolusHistory = new ArrayList<>(1); - bolusHistory.add(readBolusRecord()); - result.history = new PumpHistory().bolusHistory(bolusHistory); + if (numberOfBolusRecordsToRetrieve > 0) { + // navigate to bolus data menu + scripter.pressCheckKey(); + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA); + List bolusHistory = new ArrayList<>(numberOfBolusRecordsToRetrieve); + result.history = new PumpHistory().bolusHistory(bolusHistory); + for(int recordsLeftToRead = numberOfBolusRecordsToRetrieve; recordsLeftToRead > 0; recordsLeftToRead--) { + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA); + bolusHistory.add(readBolusRecord()); + int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + scripter.pressDownKey(); + while (record == (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD)) { + scripter.waitForScreenUpdate(); + } + } + if (log.isDebugEnabled()) { + if (!result.history.bolusHistory.isEmpty()) { + log.debug("Read bolus history (" + result.history.bolusHistory.size() + "):"); + for (Bolus bolus : result.history.bolusHistory) { + log.debug(new Date(bolus.timestamp) + ": " + bolus.toString()); + } + } + } + } scripter.returnToRootMenu(); result.success = true; } From 6fded4c1bd7a5c0a7b8d61c8df74d2a81ae50478 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 19:32:00 +0100 Subject: [PATCH 03/16] ComboPlugin.deliverBolus: avoid creating boluses within the same minute. --- .../plugins/PumpCombo/ComboPlugin.java | 90 ++++++++++++------- 1 file changed, 58 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index bf8d91bac1..f254858b79 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -489,42 +489,68 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf @NonNull private PumpEnactResult deliverBolus(final DetailedBolusInfo detailedBolusInfo) { - // Guard against boluses issued multiple times within two minutes. - // Two minutes, so that the resulting timestamp and bolus are different with the Combo - // history records which only store with minute-precision - if (lastRequestedBolus != null - && Math.abs(lastRequestedBolus.amount - detailedBolusInfo.insulin) < 0.01 - && lastRequestedBolus.timestamp + 120 * 1000 > System.currentTimeMillis()) { - log.error("Bolus request rejected, same bolus requested recently: " + lastRequestedBolus); - return new PumpEnactResult().success(false).enacted(false) - .comment(MainApp.gs(R.string.bolus_frequency_exceeded)); - } - lastRequestedBolus = new Bolus(System.currentTimeMillis(), detailedBolusInfo.insulin, true); - - // check pump is ready and all pump bolus records are known - CommandResult stateResult = runCommand(null, 2, ruffyScripter::readQuickInfo); - if (!stateResult.success) { - return new PumpEnactResult().success(false).enacted(false) - .comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered)); - } - if (stateResult.reservoirLevel != -1 && stateResult.reservoirLevel - 0.5 < detailedBolusInfo.insulin) { - return new PumpEnactResult().success(false).enacted(false) - .comment(MainApp.gs(R.string.combo_reservoir_level_insufficient_for_bolus)); - } - // the commands above ensured a connection was made, which updated this field - if (pumpHistoryChanged) { - return new PumpEnactResult().success(false).enacted(false) - .comment(MainApp.gs(R.string.combo_bolus_rejected_due_to_pump_history_change)); - } - - Bolus previousBolus = stateResult.history != null && !stateResult.history.bolusHistory.isEmpty() - ? stateResult.history.bolusHistory.get(0) - : new Bolus(0, 0, false); - try { pump.activity = MainApp.gs(R.string.combo_pump_action_bolusing, detailedBolusInfo.insulin); MainApp.bus().post(new EventComboPumpUpdateGUI()); + // Guard against boluses issued multiple times within two minutes. + // Two minutes, so that the resulting timestamp and bolus are different with the Combo + // history records which only store with minute-precision + if (lastRequestedBolus != null + && Math.abs(lastRequestedBolus.amount - detailedBolusInfo.insulin) < 0.01 + && lastRequestedBolus.timestamp + 120 * 1000 > System.currentTimeMillis()) { + log.error("Bolus request rejected, same bolus requested recently: " + lastRequestedBolus); + return new PumpEnactResult().success(false).enacted(false) + .comment(MainApp.gs(R.string.bolus_frequency_exceeded)); + } + lastRequestedBolus = new Bolus(System.currentTimeMillis(), detailedBolusInfo.insulin, true); + + // check pump is ready and all pump bolus records are known + CommandResult stateResult = runCommand(null, 2, () -> ruffyScripter.readQuickInfo(1)); + if (!stateResult.success) { + return new PumpEnactResult().success(false).enacted(false) + .comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered)); + } + if (stateResult.reservoirLevel != -1 && stateResult.reservoirLevel - 0.5 < detailedBolusInfo.insulin) { + return new PumpEnactResult().success(false).enacted(false) + .comment(MainApp.gs(R.string.combo_reservoir_level_insufficient_for_bolus)); + } + // the commands above ensured a connection was made, which updated this field + if (pumpHistoryChanged) { + return new PumpEnactResult().success(false).enacted(false) + .comment(MainApp.gs(R.string.combo_bolus_rejected_due_to_pump_history_change)); + } + + Bolus previousBolus = stateResult.history != null && !stateResult.history.bolusHistory.isEmpty() + ? stateResult.history.bolusHistory.get(0) + : new Bolus(0, 0, false); + + // if the last bolus was given in the current minute, wait till the pump clock moves + // to the next minute to ensure timestamps are unique and can be imported + CommandResult timeCheckResult = ruffyScripter.readPumpState(); + long maxWaitTimeout = System.currentTimeMillis() + 65 * 1000; + int waitLoops = 0; + while (previousBolus.timestamp == timeCheckResult.state.pumpTime + && maxWaitTimeout < System.currentTimeMillis()) { + if (cancelBolus) { + return new PumpEnactResult().success(true).enacted(false); + } + if (!timeCheckResult.success) { + return new PumpEnactResult().success(false).enacted(false) + .comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered)); + } + SystemClock.sleep(5000); + timeCheckResult = ruffyScripter.readPumpState(); + waitLoops++; + } + if (waitLoops > 0) { + Answers.getInstance().logCustom(new CustomEvent("ComboBolusTimestampWait") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("waitTimeSecs", String.valueOf(waitLoops * 5))); + log.debug("Waited " + (waitLoops * 5) + "s for pump to switch to a fresh minute before bolusing"); + } + if (cancelBolus) { return new PumpEnactResult().success(true).enacted(false); } From 1e8e2a59fd476c365432b158b4d517142844e4a4 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 19:56:27 +0100 Subject: [PATCH 04/16] ReadHistoryCommand: fetch records including the requested timestamp. To ensure we retrieve records with the same timestamp. --- .../ruffyscripter/commands/ReadHistoryCommand.java | 9 ++++----- .../ruffyscripter/history/PumpHistoryRequest.java | 6 ++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadHistoryCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadHistoryCommand.java index 1cf5b86e28..6af1b99dee 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadHistoryCommand.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadHistoryCommand.java @@ -4,7 +4,6 @@ import android.support.annotation.NonNull; import org.monkey.d.ruffy.ruffy.driver.display.MenuAttribute; import org.monkey.d.ruffy.ruffy.driver.display.MenuType; -import org.monkey.d.ruffy.ruffy.driver.display.menu.BolusType; import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuDate; import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime; import org.slf4j.Logger; @@ -145,7 +144,7 @@ public class ReadHistoryCommand extends BaseCommand { int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); while (true) { Tdd tdd = readTddRecord(); - if (requestedTime != PumpHistoryRequest.FULL && tdd.timestamp <= requestedTime) { + if (requestedTime != PumpHistoryRequest.FULL && tdd.timestamp < requestedTime) { break; } log.debug("Read TDD record #" + record + "/" + totalRecords); @@ -183,7 +182,7 @@ public class ReadHistoryCommand extends BaseCommand { int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); while (true) { Tbr tbr = readTbrRecord(); - if (requestedTime != PumpHistoryRequest.FULL && tbr.timestamp <= requestedTime) { + if (requestedTime != PumpHistoryRequest.FULL && tbr.timestamp < requestedTime) { break; } log.debug("Read TBR record #" + record + "/" + totalRecords); @@ -215,7 +214,7 @@ public class ReadHistoryCommand extends BaseCommand { int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); while (true) { Bolus bolus = readBolusRecord(); - if (requestedTime != PumpHistoryRequest.FULL && bolus.timestamp <= requestedTime) { + if (requestedTime != PumpHistoryRequest.FULL && bolus.timestamp < requestedTime) { break; } log.debug("Read bolus record #" + record + "/" + totalRecords); @@ -237,7 +236,7 @@ public class ReadHistoryCommand extends BaseCommand { int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); while (true) { PumpAlert error = readAlertRecord(); - if (requestedTime != PumpHistoryRequest.FULL && error.timestamp <= requestedTime) { + if (requestedTime != PumpHistoryRequest.FULL && error.timestamp < requestedTime) { break; } log.debug("Read alert record #" + record + "/" + totalRecords); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistoryRequest.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistoryRequest.java index 40dd6e553e..e785c1d67c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistoryRequest.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistoryRequest.java @@ -5,8 +5,10 @@ import java.util.Date; /** What data a 'read history' request should return. */ public class PumpHistoryRequest { /* History to read: - Either the timestamp of the last known record to fetch all newer records, - or one of the constants to read no history or all of it. + Either the timestamp of the last known record or one of the constants to read no history + or all of it. When a timestamp is provided all newer records and records matching the + timestamp are returned. Returning all records equal to the timestamp ensures a record + with a duplicate timestamp is also detected as a new record. */ public static final long LAST = -2; public static final long SKIP = -1; From 183edfcc095c117ad8a2f9a83e962f47faefbe36 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 20:03:30 +0100 Subject: [PATCH 05/16] When checking for changed pump history, compare the last two records. This ensures changes are also detected if a bolus was added on the pump within the same minute of the previous record. --- .../plugins/PumpCombo/ComboPlugin.java | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index f254858b79..6961dcaa80 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -11,8 +11,10 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.List; import info.nightscout.androidaps.BuildConfig; import info.nightscout.androidaps.MainApp; @@ -99,6 +101,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf private volatile boolean bolusInProgress; private volatile boolean cancelBolus; + /** Used to reject boluses with the same amount requested within two minutes */ private Bolus lastRequestedBolus; /** @@ -106,10 +109,12 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf * records on the pump have been found. This effectively blocks high temps and boluses * till the queue is empty and the connection is shut down. The next reconnect will * then reset this flag. This might cause some grief when attempting to bolus again within - * the 5s of idling it takes before the connecting is shut down. + * the 5s of idling it takes before the connecting is shut down. Or if the queue is very + * large, giving the user more time to input boluses. I don't have a good solution for this + * at the moment, but this is edge-casey to not address this at this point. */ private volatile boolean pumpHistoryChanged = false; - private volatile long timestampOfLastKnownPumpBolusRecord; + private volatile List recentBoluses = new ArrayList<>(0); public static ComboPlugin getPlugin() { if (plugin == null) @@ -568,7 +573,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf // history below to see what was actually delivered // get last bolus from pump history for verification - CommandResult postBolusStateResult = runCommand(null, 3, () -> ruffyScripter.readQuickInfo(1)); + CommandResult postBolusStateResult = runCommand(null, 3, () -> ruffyScripter.readQuickInfo(2)); if (!postBolusStateResult.success) { return new PumpEnactResult().success(false).enacted(false) .comment(MainApp.gs(R.string.combo_error_bolus_verification_failed)); @@ -594,7 +599,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf return new PumpEnactResult().success(false).enacted(true) .comment(MainApp.gs(R.string.combo_error_updating_treatment_record)); - timestampOfLastKnownPumpBolusRecord = lastPumpBolus.timestamp; + recentBoluses = postBolusStateResult.history.bolusHistory; // only a partial bolus was delivered if (Math.abs(lastPumpBolus.amount - detailedBolusInfo.insulin) > 0.01) { @@ -1154,26 +1159,36 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf * @return null on success or the failed command result */ private CommandResult checkHistory() { - CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3, ruffyScripter::readQuickInfo); - if (quickInfoResult.history != null && !quickInfoResult.history.bolusHistory.isEmpty() - && quickInfoResult.history.bolusHistory.get(0).timestamp == timestampOfLastKnownPumpBolusRecord) { + CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3, () -> ruffyScripter.readQuickInfo(2)); + + // no history, nothing to check or complain about + if (quickInfoResult.history == null || quickInfoResult.history.bolusHistory.isEmpty()) { return null; } - // OPTIMIZE this reads the entire history on start, so this could be optimized by persisting - // `timestampOfLastKnownPumpBolusRecord`, though this should be thought through, to make sure - // all scenarios are covered + // compare recent records + List existingbolusHistory = quickInfoResult.history.bolusHistory; + if (existingbolusHistory.size() == 1 && recentBoluses.size() == 1 + && quickInfoResult.history.bolusHistory.get(0).equals(recentBoluses.get(0))) { + return null; + } else if (existingbolusHistory.size() == 2 && recentBoluses.size() == 2 + && quickInfoResult.history.bolusHistory.get(0).equals(recentBoluses.get(0)) + && quickInfoResult.history.bolusHistory.get(1).equals(recentBoluses.get(1))) { + return null; + } + + // fetch new records CommandResult historyResult = runCommand(MainApp.gs(R.string.combo_activity_reading_pump_history), 3, () -> - ruffyScripter.readHistory(new PumpHistoryRequest() - .bolusHistory(timestampOfLastKnownPumpBolusRecord))); + ruffyScripter.readHistory(new PumpHistoryRequest().bolusHistory(existingbolusHistory.get(0).timestamp))); if (!historyResult.success) { return historyResult; } pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history); - if (!historyResult.history.bolusHistory.isEmpty()) { - timestampOfLastKnownPumpBolusRecord = historyResult.history.bolusHistory.get(0).timestamp; + List updatedBolusHistory = historyResult.history.bolusHistory; + if (!updatedBolusHistory.isEmpty()) { + recentBoluses = updatedBolusHistory.subList(0, Math.min(updatedBolusHistory.size(), 2)); } return null; From 2a5c28cf6bf1ee7d481d9ff41ba34e3f4586ff43 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 22:00:56 +0100 Subject: [PATCH 06/16] ReadQuickInfoCommand: only read as many boluses as are available. (cherry picked from commit 7857abb) --- .../ruffyscripter/commands/ReadQuickInfoCommand.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java index 2973091c67..e55fb4d010 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java @@ -36,14 +36,20 @@ public class ReadQuickInfoCommand extends BaseCommand { scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA); List bolusHistory = new ArrayList<>(numberOfBolusRecordsToRetrieve); result.history = new PumpHistory().bolusHistory(bolusHistory); - for(int recordsLeftToRead = numberOfBolusRecordsToRetrieve; recordsLeftToRead > 0; recordsLeftToRead--) { - scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA); + // read bolus records + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + while (true) { bolusHistory.add(readBolusRecord()); - int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + if (bolusHistory.size() == numberOfBolusRecordsToRetrieve || record == totalRecords) { + break; + } + // advance to next record scripter.pressDownKey(); while (record == (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD)) { scripter.waitForScreenUpdate(); } + record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); } if (log.isDebugEnabled()) { if (!result.history.bolusHistory.isEmpty()) { From 956a611af4a5394649f44b1e2d1f83274b8dd898 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 22:01:44 +0100 Subject: [PATCH 07/16] Fix checking/updating history. (cherry picked from commit 5413a0f) --- .../plugins/PumpCombo/ComboPlugin.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index 6961dcaa80..75da597bf5 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -409,7 +409,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf // ComboFragment updates state fully only after the pump has initialized, // so force an update after initialization completed - updateLocalData(runCommand(null, 1, () -> ruffyScripter.readQuickInfo(1))); + MainApp.bus().post(new EventComboPumpUpdateGUI()); } /** Updates local cache with state (reservoir level, last bolus ...) returned from the pump */ @@ -532,7 +532,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf // if the last bolus was given in the current minute, wait till the pump clock moves // to the next minute to ensure timestamps are unique and can be imported - CommandResult timeCheckResult = ruffyScripter.readPumpState(); + CommandResult timeCheckResult = runCommand(null, 0, ruffyScripter::readPumpState); long maxWaitTimeout = System.currentTimeMillis() + 65 * 1000; int waitLoops = 0; while (previousBolus.timestamp == timeCheckResult.state.pumpTime @@ -545,7 +545,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf .comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered)); } SystemClock.sleep(5000); - timeCheckResult = ruffyScripter.readPumpState(); + timeCheckResult = runCommand(null, 0, ruffyScripter::readPumpState); waitLoops++; } if (waitLoops > 0) { @@ -1167,28 +1167,29 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf } // compare recent records - List existingbolusHistory = quickInfoResult.history.bolusHistory; - if (existingbolusHistory.size() == 1 && recentBoluses.size() == 1 - && quickInfoResult.history.bolusHistory.get(0).equals(recentBoluses.get(0))) { + List initialPumpBolusHistory = quickInfoResult.history.bolusHistory; + if (recentBoluses.size() == 1 && initialPumpBolusHistory.size() >= 1 + && recentBoluses.get(0).equals(quickInfoResult.history.bolusHistory.get(0))) { return null; - } else if (existingbolusHistory.size() == 2 && recentBoluses.size() == 2 - && quickInfoResult.history.bolusHistory.get(0).equals(recentBoluses.get(0)) - && quickInfoResult.history.bolusHistory.get(1).equals(recentBoluses.get(1))) { + } else if (recentBoluses.size() == 2 && initialPumpBolusHistory.size() >= 2 + && recentBoluses.get(0).equals(quickInfoResult.history.bolusHistory.get(0)) + && recentBoluses.get(1).equals(quickInfoResult.history.bolusHistory.get(1))) { return null; } // fetch new records + long lastKnownPumpRecordTimestamp = recentBoluses.isEmpty() ? 0 : recentBoluses.get(0).timestamp; CommandResult historyResult = runCommand(MainApp.gs(R.string.combo_activity_reading_pump_history), 3, () -> - ruffyScripter.readHistory(new PumpHistoryRequest().bolusHistory(existingbolusHistory.get(0).timestamp))); + ruffyScripter.readHistory(new PumpHistoryRequest().bolusHistory(lastKnownPumpRecordTimestamp))); if (!historyResult.success) { return historyResult; } pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history); - List updatedBolusHistory = historyResult.history.bolusHistory; - if (!updatedBolusHistory.isEmpty()) { - recentBoluses = updatedBolusHistory.subList(0, Math.min(updatedBolusHistory.size(), 2)); + List updatedPumpBolusHistory = historyResult.history.bolusHistory; + if (!updatedPumpBolusHistory.isEmpty()) { + recentBoluses = updatedPumpBolusHistory.subList(0, Math.min(updatedPumpBolusHistory.size(), 2)); } return null; From 87771e8753878bbc522b526e01bfe1ef18b436ed Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sat, 3 Feb 2018 23:44:17 +0100 Subject: [PATCH 08/16] A few fixes. --- .../androidaps/plugins/PumpCombo/ComboPlugin.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index 75da597bf5..79f5e08a14 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -532,8 +532,9 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf // if the last bolus was given in the current minute, wait till the pump clock moves // to the next minute to ensure timestamps are unique and can be imported - CommandResult timeCheckResult = runCommand(null, 0, ruffyScripter::readPumpState); - long maxWaitTimeout = System.currentTimeMillis() + 65 * 1000; + CommandResult timeCheckResult = stateResult; + long waitStartTime = System.currentTimeMillis(); + long maxWaitTimeout = waitStartTime + 65 * 1000; int waitLoops = 0; while (previousBolus.timestamp == timeCheckResult.state.pumpTime && maxWaitTimeout < System.currentTimeMillis()) { @@ -544,16 +545,17 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf return new PumpEnactResult().success(false).enacted(false) .comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered)); } - SystemClock.sleep(5000); + SystemClock.sleep(2000); timeCheckResult = runCommand(null, 0, ruffyScripter::readPumpState); waitLoops++; } if (waitLoops > 0) { + long waitDuration = (System.currentTimeMillis() - waitStartTime) / 1000; Answers.getInstance().logCustom(new CustomEvent("ComboBolusTimestampWait") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) - .putCustomAttribute("waitTimeSecs", String.valueOf(waitLoops * 5))); - log.debug("Waited " + (waitLoops * 5) + "s for pump to switch to a fresh minute before bolusing"); + .putCustomAttribute("waitTimeSecs", String.valueOf(waitDuration))); + log.debug("Waited " + waitDuration + "s for pump to switch to a fresh minute before bolusing"); } if (cancelBolus) { @@ -811,9 +813,11 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf CommandResult commandResult; try { if (!ruffyScripter.isConnected()) { + String originalActivity = pump.activity; pump.activity = MainApp.gs(R.string.combo_activity_checking_pump_state); MainApp.bus().post(new EventComboPumpUpdateGUI()); CommandResult preCheckError = runOnConnectChecks(); + pump.activity = originalActivity; if (preCheckError != null) { updateLocalData(preCheckError); return preCheckError; From 8c25bc4002aaef9adfaa3c759eab8de41861e6a6 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sun, 4 Feb 2018 01:29:27 +0100 Subject: [PATCH 09/16] Answers cleanup. --- .../PumpCombo/ruffyscripter/RuffyScripter.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java index 4112306fdd..1020f2bb4f 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java @@ -428,7 +428,7 @@ public class RuffyScripter implements RuffyCommands { Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromConnectionLoss") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) - .putCustomAttribute("activeCommand", "" + activeCmd) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) .putCustomAttribute("success", connected ? "true" : "else")); return connected; } @@ -443,7 +443,8 @@ public class RuffyScripter implements RuffyCommands { Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) - .putCustomAttribute("activeCommand", "" + activeCmd) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) + .putCustomAttribute("exit", "1") .putCustomAttribute("success", "false")); return new PumpState(); } @@ -461,15 +462,18 @@ public class RuffyScripter implements RuffyCommands { Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) - .putCustomAttribute("activeCommand", "" + activeCmd) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) + .putCustomAttribute("exit", "2") .putCustomAttribute("success", "true")); return pumpState; } catch (Exception e) { Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) - .putCustomAttribute("activeCommand", "" + activeCmd) + .putCustomAttribute("exit", "3") + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) .putCustomAttribute("success", "false")); + log.debug("Reading pump state during recovery failed", e); return new PumpState(); } @@ -494,7 +498,7 @@ public class RuffyScripter implements RuffyCommands { Answers.getInstance().logCustom(new CustomEvent("ComboConnectTimeout") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) - .putCustomAttribute("activeCommand", "" + activeCmd) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) .putCustomAttribute("previousCommand", previousCommand)); throw new CommandException("Timeout connecting to pump"); } From 4937018a9c0f934b419ac97393246cc3612bb3d1 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sun, 4 Feb 2018 20:15:39 +0100 Subject: [PATCH 10/16] Add test options to build.gradle to unbreak unit tests. --- app/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index e488472e52..1c7352dceb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,6 +162,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + testOptions { + unitTests.returnDefaultValues = true + } } allprojects { From 5be9bf5a87bbc30ec0329be554d02a6b42318aba Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sun, 4 Feb 2018 20:43:31 +0100 Subject: [PATCH 11/16] Combo: add unit test for calculateFakeBolusDate. --- .../plugins/PumpCombo/ComboPluginTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPluginTest.java diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPluginTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPluginTest.java new file mode 100644 index 0000000000..a87cbbfbd4 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPluginTest.java @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.plugins.PumpCombo; + +import android.content.Context; + +import com.squareup.otto.Bus; +import com.squareup.otto.ThreadEnforcer; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; +import info.nightscout.utils.ToastUtils; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({MainApp.class, ConfigBuilderPlugin.class, ConfigBuilderPlugin.class, ToastUtils.class, Context.class}) +public class ComboPluginTest { + + @Before + public void prepareMocks() throws Exception { + ConfigBuilderPlugin configBuilderPlugin = mock(ConfigBuilderPlugin.class); + PowerMockito.mockStatic(ConfigBuilderPlugin.class); + + PowerMockito.mockStatic(MainApp.class); + MainApp mainApp = mock(MainApp.class); + when(MainApp.getConfigBuilder()).thenReturn(configBuilderPlugin); + when(MainApp.instance()).thenReturn(mainApp); + Bus bus = new Bus(ThreadEnforcer.ANY); + when(MainApp.bus()).thenReturn(bus); + when(MainApp.gs(0)).thenReturn(""); + } + + @Test + public void calculateFakePumpTimestamp() throws Exception { + ComboPlugin plugin = ComboPlugin.getPlugin(); + long now = System.currentTimeMillis(); + long pumpTimestamp = now - now % 1000; + // same timestamp, different bolus leads to different fake timestamp + Assert.assertNotEquals( + plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp, 0.1, true)), + plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp, 0.3, true)) + ); + // different timestamp, same bolus leads to different fake timestamp + Assert.assertNotEquals( + plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp, 0.3, true)), + plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp + 60 * 1000, 0.3, true)) + ); + // generated timestamp has second-precision + Bolus bolus = new Bolus(pumpTimestamp, 0.2, true); + long calculatedTimestamp = plugin.calculateFakeBolusDate(bolus); + assertEquals(calculatedTimestamp, calculatedTimestamp - calculatedTimestamp % 1000); + } +} \ No newline at end of file From f07017a4f637eb7ea540df48b4ce60301687bd31 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sun, 4 Feb 2018 20:58:08 +0100 Subject: [PATCH 12/16] Document global state variables. --- .../plugins/PumpCombo/ComboPlugin.java | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index 79f5e08a14..1dcc645b48 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -98,22 +98,38 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf @NonNull private static final ComboPump pump = new ComboPump(); - private volatile boolean bolusInProgress; + /** This is used to determine when to pass a bolus cancel request to the scripter */ + private volatile boolean scripterIsBolusing; + /** This is set to true to request a bolus cancellation. {@link #deliverBolus(DetailedBolusInfo)} + * will reset this flag. */ private volatile boolean cancelBolus; - /** Used to reject boluses with the same amount requested within two minutes */ + /** Used to reject boluses with the same amount requested within two minutes. + * Used solely by {@link #deliverBolus(DetailedBolusInfo)}. This is independent of the + * pump history and is meant as a safety feature to block multiple requests due to an + * application bug. Whether the requested bolus was delivered once is not taken into account. */ private Bolus lastRequestedBolus; /** - * This is set whenever a connection to the pump is made and indicates if new history - * records on the pump have been found. This effectively blocks high temps and boluses - * till the queue is empty and the connection is shut down. The next reconnect will - * then reset this flag. This might cause some grief when attempting to bolus again within - * the 5s of idling it takes before the connecting is shut down. Or if the queue is very - * large, giving the user more time to input boluses. I don't have a good solution for this - * at the moment, but this is edge-casey to not address this at this point. + * This is set (in {@link #checkHistory()} whenever a connection to the pump is made and + * indicates if new history records on the pump have been found. This effectively blocks + * high temps ({@link #setTempBasalPercent(Integer, Integer)} and boluses + * ({@link #deliverBolus(DetailedBolusInfo)} till the queue is empty and the connection + * is shut down. + * {@link #initializePump()} resets this since on startup the history is allowed to have + * changed (and the user can't possible have already calculated anything with out of date IOB). + * The next reconnect will then reset this flag. This might cause some grief when attempting + * to bolus again within the 5s of idling it takes before the connecting is shut down. Or if + * the queue is very large, giving the user more time to input boluses. I don't have a good + * solution for this at the moment, but this is enough of an edge case - faulting in the right + * direction - so that adding more complexity yields little benefit. */ private volatile boolean pumpHistoryChanged = false; + + /** Cache of the last <=2 boluses on the pump. Used to detect changes in pump history, + * requiring reading pump more history. This is read/set in {@link #checkHistory()} when changed + * pump history was detected and was read, as well as in {@link #deliverBolus(DetailedBolusInfo)} + * after bolus delivery. */ private volatile List recentBoluses = new ArrayList<>(0); public static ComboPlugin getPlugin() { @@ -123,7 +139,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf } private static final PumpEnactResult OPERATION_NOT_SUPPORTED = new PumpEnactResult() - .success(false).enacted(false).comment(MainApp.sResources.getString(R.string.combo_pump_unsupported_operation)); + .success(false).enacted(false).comment(MainApp.gs(R.string.combo_pump_unsupported_operation)); private ComboPlugin() { ruffyScripter = new RuffyScripter(MainApp.instance().getApplicationContext()); @@ -545,6 +561,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf return new PumpEnactResult().success(false).enacted(false) .comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered)); } + log.debug("Waiting for pump clock to advance for the next unused bolus record timestamp"); SystemClock.sleep(2000); timeCheckResult = runCommand(null, 0, ruffyScripter::readPumpState); waitLoops++; @@ -565,16 +582,17 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf BolusProgressReporter progressReporter = detailedBolusInfo.isSMB ? nullBolusProgressReporter : bolusProgressReporter; // start bolus delivery - bolusInProgress = true; + scripterIsBolusing = true; runCommand(null, 0, () -> ruffyScripter.deliverBolus(detailedBolusInfo.insulin, progressReporter)); - bolusInProgress = false; + scripterIsBolusing = false; // Note that the result of the issued bolus command is not checked. If there was // a connection problem, ruffyscripter tried to recover and we can just check the // history below to see what was actually delivered // get last bolus from pump history for verification + // (reads 2 records to update `recentBoluses` further down) CommandResult postBolusStateResult = runCommand(null, 3, () -> ruffyScripter.readQuickInfo(2)); if (!postBolusStateResult.success) { return new PumpEnactResult().success(false).enacted(false) @@ -643,7 +661,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf if (!treatmentCreated) { log.error("Adding treatment record overrode an existing record: " + dbi); if (dbi.isSMB) { - Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.sResources.getString(R.string.combo_error_updating_treatment_record), Notification.URGENT); + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_error_updating_treatment_record), Notification.URGENT); MainApp.bus().post(new EventNewNotification(notification)); } Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError") @@ -655,7 +673,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf } catch (Exception e) { log.error("Adding treatment record failed", e); if (dbi.isSMB) { - Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.sResources.getString(R.string.combo_error_updating_treatment_record), Notification.URGENT); + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_error_updating_treatment_record), Notification.URGENT); MainApp.bus().post(new EventNewNotification(notification)); Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) @@ -669,7 +687,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf @Override public void stopBolusDelivering() { - if (bolusInProgress) { + if (scripterIsBolusing) { ruffyScripter.cancelBolus(); } cancelBolus = true; @@ -1107,7 +1125,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf * Should be good enough, even with command mode, it's a challenge to create that situation * and most time clashes will be around SMBs which are covered. */ - private long calculateFakeBolusDate(Bolus pumpBolus) { + long calculateFakeBolusDate(Bolus pumpBolus) { double bolus = pumpBolus.amount - 0.1; int secondsFromBolus = (int) (bolus * 10 * 1000); return pumpBolus.timestamp + Math.min(secondsFromBolus, 59 * 1000); From e812c7119f53752e677e9edfcf1acad75aec18cc Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Sun, 4 Feb 2018 23:01:30 +0100 Subject: [PATCH 13/16] Fix setting 'pumpHistoryChanged'. --- .../androidaps/plugins/PumpCombo/ComboPlugin.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index 1dcc645b48..ec0e86a3d9 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -619,6 +619,8 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf return new PumpEnactResult().success(false).enacted(true) .comment(MainApp.gs(R.string.combo_error_updating_treatment_record)); + // update `recentBoluses` so the bolus was just delivered won't be detected as an new + // bolus that has been delivered on the pump recentBoluses = postBolusStateResult.history.bolusHistory; // only a partial bolus was delivered @@ -1181,10 +1183,13 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf * @return null on success or the failed command result */ private CommandResult checkHistory() { - CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3, () -> ruffyScripter.readQuickInfo(2)); + CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3, + () -> ruffyScripter.readQuickInfo(2)); // no history, nothing to check or complain about if (quickInfoResult.history == null || quickInfoResult.history.bolusHistory.isEmpty()) { + log.debug("Setting 'pumpHistoryChanged' false"); + pumpHistoryChanged = false; return null; } @@ -1192,10 +1197,14 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf List initialPumpBolusHistory = quickInfoResult.history.bolusHistory; if (recentBoluses.size() == 1 && initialPumpBolusHistory.size() >= 1 && recentBoluses.get(0).equals(quickInfoResult.history.bolusHistory.get(0))) { + log.debug("Setting 'pumpHistoryChanged' false"); + pumpHistoryChanged = false; return null; } else if (recentBoluses.size() == 2 && initialPumpBolusHistory.size() >= 2 && recentBoluses.get(0).equals(quickInfoResult.history.bolusHistory.get(0)) && recentBoluses.get(1).equals(quickInfoResult.history.bolusHistory.get(1))) { + log.debug("Setting 'pumpHistoryChanged' false"); + pumpHistoryChanged = false; return null; } @@ -1204,10 +1213,14 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf CommandResult historyResult = runCommand(MainApp.gs(R.string.combo_activity_reading_pump_history), 3, () -> ruffyScripter.readHistory(new PumpHistoryRequest().bolusHistory(lastKnownPumpRecordTimestamp))); if (!historyResult.success) { + pumpHistoryChanged = true; return historyResult; } pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history); + if (pumpHistoryChanged) { + log.debug("Setting 'pumpHistoryChanged' true"); + } List updatedPumpBolusHistory = historyResult.history.bolusHistory; if (!updatedPumpBolusHistory.isEmpty()) { From 5b21844423a1bafce8670116b48130b015a4450d Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Mon, 5 Feb 2018 22:59:25 +0100 Subject: [PATCH 14/16] Did it again. --- .../nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index ec0e86a3d9..a646d00af7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -553,7 +553,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf long maxWaitTimeout = waitStartTime + 65 * 1000; int waitLoops = 0; while (previousBolus.timestamp == timeCheckResult.state.pumpTime - && maxWaitTimeout < System.currentTimeMillis()) { + && maxWaitTimeout > System.currentTimeMillis()) { if (cancelBolus) { return new PumpEnactResult().success(true).enacted(false); } @@ -619,7 +619,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf return new PumpEnactResult().success(false).enacted(true) .comment(MainApp.gs(R.string.combo_error_updating_treatment_record)); - // update `recentBoluses` so the bolus was just delivered won't be detected as an new + // update `recentBoluses` so the bolus was just delivered won't be detected as a new // bolus that has been delivered on the pump recentBoluses = postBolusStateResult.history.bolusHistory; From a861afa208759b3b6dbd257de1adf8a1d4ed5594 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Mon, 5 Feb 2018 23:19:43 +0100 Subject: [PATCH 15/16] Log bolus amount when saving to DB failed. --- .../nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index a646d00af7..372049fedf 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -669,6 +669,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("bolus", String.valueOf(lastPumpBolus.amount)) .putCustomAttribute("issue", "record with same timestamp existed and was overridden")); return false; } @@ -680,6 +681,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError") .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("bolus", String.valueOf(lastPumpBolus.amount)) .putCustomAttribute("issue", "adding record caused exception")); } return false; From e9f0fa3c04a3a2978209316a67a0322badeef085 Mon Sep 17 00:00:00 2001 From: Johannes Mockenhaupt Date: Mon, 5 Feb 2018 23:58:19 +0100 Subject: [PATCH 16/16] Warn when finding multiple boluses in the same minute with same amount. Not much can be done if the user manages to bolus multiple boluses within the same minute, with the same amount. This should almost never happen, but if it does, at least warn the user one of those boluses isn't accounted for wrt to IOB. --- .../plugins/PumpCombo/ComboPlugin.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java index 372049fedf..176fcba67f 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.HashSet; import java.util.List; import info.nightscout.androidaps.BuildConfig; @@ -1219,6 +1220,23 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf return historyResult; } + // Check edge of multiple boluses with the same amount in the same minute being imported. + // This is about as edgy-casey as it can get. I'd be surprised of this one actually ever + // triggers. It might, so at least give a warning, since a delivered bolus isn't accounted + // for. + HashSet bolusSet = new HashSet<>(historyResult.history.bolusHistory); + if (bolusSet.size() != historyResult.history.bolusHistory.size()) { + log.debug("Bolus with same amount within the same minute imported. Only one will make it to the DB."); + Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("bolus", "") + .putCustomAttribute("issue", "multiple pump history records with the same time and amount")); + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string. + combo_error_multiple_boluses_with_idential_timestamp), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + } + pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history); if (pumpHistoryChanged) { log.debug("Setting 'pumpHistoryChanged' true");