Merge pull request #26 from jotomo/combo-scripter-v2

feb 6
This commit is contained in:
Simon Pauwels 2018-02-06 00:06:51 +01:00 committed by GitHub
commit 5093b7b815
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 297 additions and 95 deletions

View file

@ -162,6 +162,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions {
unitTests.returnDefaultValues = true
}
}
allprojects {

View file

@ -11,8 +11,11 @@ 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.HashSet;
import java.util.List;
import info.nightscout.androidaps.BuildConfig;
import info.nightscout.androidaps.MainApp;
@ -96,20 +99,39 @@ 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 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.
* 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;
private volatile long timestampOfLastKnownPumpBolusRecord;
/** 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<Bolus> recentBoluses = new ArrayList<>(0);
public static ComboPlugin getPlugin() {
if (plugin == null)
@ -118,7 +140,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());
@ -404,7 +426,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));
MainApp.bus().post(new EventComboPumpUpdateGUI());
}
/** Updates local cache with state (reservoir level, last bolus ...) returned from the pump */
@ -489,6 +511,10 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
@NonNull
private PumpEnactResult deliverBolus(final DetailedBolusInfo detailedBolusInfo) {
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
@ -502,7 +528,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
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);
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));
@ -521,9 +547,34 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
? 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());
// 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 = stateResult;
long waitStartTime = System.currentTimeMillis();
long maxWaitTimeout = waitStartTime + 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));
}
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++;
}
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(waitDuration)));
log.debug("Waited " + waitDuration + "s for pump to switch to a fresh minute before bolusing");
}
if (cancelBolus) {
return new PumpEnactResult().success(true).enacted(false);
@ -532,17 +583,18 @@ 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
CommandResult postBolusStateResult = runCommand(null, 3, ruffyScripter::readQuickInfo);
// (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)
.comment(MainApp.gs(R.string.combo_error_bolus_verification_failed));
@ -568,7 +620,9 @@ 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;
// 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;
// only a partial bolus was delivered
if (Math.abs(lastPumpBolus.amount - detailedBolusInfo.insulin) > 0.01) {
@ -602,7 +656,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 {
@ -610,23 +664,25 @@ 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")
.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;
}
} 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)
.putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("bolus", String.valueOf(lastPumpBolus.amount))
.putCustomAttribute("issue", "adding record caused exception"));
}
return false;
@ -636,7 +692,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
@Override
public void stopBolusDelivering() {
if (bolusInProgress) {
if (scripterIsBolusing) {
ruffyScripter.cancelBolus();
}
cancelBolus = true;
@ -780,9 +836,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;
@ -1055,7 +1113,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 +1125,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));
long calculateFakeBolusDate(Bolus pumpBolus) {
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
@ -1126,26 +1186,65 @@ 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()) {
log.debug("Setting 'pumpHistoryChanged' false");
pumpHistoryChanged = false;
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<Bolus> 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;
}
// 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(timestampOfLastKnownPumpBolusRecord)));
ruffyScripter.readHistory(new PumpHistoryRequest().bolusHistory(lastKnownPumpRecordTimestamp)));
if (!historyResult.success) {
pumpHistoryChanged = true;
return historyResult;
}
pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history);
// 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<Bolus> 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_identical_timestamp), Notification.URGENT);
MainApp.bus().post(new EventNewNotification(notification));
}
if (!historyResult.history.bolusHistory.isEmpty()) {
timestampOfLastKnownPumpBolusRecord = historyResult.history.bolusHistory.get(0).timestamp;
pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history);
if (pumpHistoryChanged) {
log.debug("Setting 'pumpHistoryChanged' true");
}
List<Bolus> updatedPumpBolusHistory = historyResult.history.bolusHistory;
if (!updatedPumpBolusHistory.isEmpty()) {
recentBoluses = updatedPumpBolusHistory.subList(0, Math.min(updatedPumpBolusHistory.size(), 2));
}
return null;

View file

@ -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. */

View file

@ -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) {
public CommandResult readQuickInfo(int numberOfBolusRecordsToRetrieve) {
Answers.getInstance().logCustom(new CustomEvent("ComboReadQuickInfoCmd")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION));
return runCommand(new ReadQuickInfoCommand());
}
Answers.getInstance().logCustom(new CustomEvent("ComboReadHistoryCmd")
.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() {
@ -436,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;
}
@ -451,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();
}
@ -469,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();
}
@ -502,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");
}

View file

@ -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);

View file

@ -2,26 +2,64 @@ 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();
if (numberOfBolusRecordsToRetrieve > 0) {
// navigate to bolus data menu
scripter.pressCheckKey();
List<Bolus> bolusHistory = new ArrayList<>(1);
bolusHistory.add(readBolusRecord());
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA);
List<Bolus> bolusHistory = new ArrayList<>(numberOfBolusRecordsToRetrieve);
result.history = new PumpHistory().bolusHistory(bolusHistory);
// 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());
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()) {
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;
}

View file

@ -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;

View file

@ -874,5 +874,6 @@
<string name="combo_warning_pump_basal_rate_changed">The basal rate on the pump has changed and will be updated soon</string>
<string name="combo_error_failure_reading_changed_basal_rate">Basal rate changed on pump, but reading it failed</string>
<string name="combo_activity_checking_for_history_changes">Checking for history changes</string>
<string name="combo_error_multiple_boluses_with_identical_timestamp">Multiple boluses with the same amount within the same minute were just imported. Only one record could be added to treatments. Please check the pump and manually add a bolus record using the Careportal tab. Make sure to create a bolus with a time no other bolus uses.</string>
</resources>

View file

@ -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);
}
}