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 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
testOptions {
unitTests.returnDefaultValues = true
}
} }
allprojects { allprojects {

View file

@ -11,8 +11,11 @@ import org.json.JSONObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List;
import info.nightscout.androidaps.BuildConfig; import info.nightscout.androidaps.BuildConfig;
import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.MainApp;
@ -96,20 +99,39 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
@NonNull @NonNull
private static final ComboPump pump = new ComboPump(); 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; 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; private Bolus lastRequestedBolus;
/** /**
* This is set whenever a connection to the pump is made and indicates if new history * This is set (in {@link #checkHistory()} whenever a connection to the pump is made and
* records on the pump have been found. This effectively blocks high temps and boluses * indicates if new history records on the pump have been found. This effectively blocks
* till the queue is empty and the connection is shut down. The next reconnect will * high temps ({@link #setTempBasalPercent(Integer, Integer)} and boluses
* then reset this flag. This might cause some grief when attempting to bolus again within * ({@link #deliverBolus(DetailedBolusInfo)} till the queue is empty and the connection
* the 5s of idling it takes before the connecting is shut down. * 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 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() { public static ComboPlugin getPlugin() {
if (plugin == null) if (plugin == null)
@ -118,7 +140,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
} }
private static final PumpEnactResult OPERATION_NOT_SUPPORTED = new PumpEnactResult() 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() { private ComboPlugin() {
ruffyScripter = new RuffyScripter(MainApp.instance().getApplicationContext()); ruffyScripter = new RuffyScripter(MainApp.instance().getApplicationContext());
@ -370,7 +392,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
} }
// trigger a connect, which will update state and check history // 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) { if (!stateResult.success) {
return; return;
} }
@ -404,7 +426,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
// ComboFragment updates state fully only after the pump has initialized, // ComboFragment updates state fully only after the pump has initialized,
// so force an update after initialization completed // 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 */ /** Updates local cache with state (reservoir level, last bolus ...) returned from the pump */
@ -489,42 +511,71 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
@NonNull @NonNull
private PumpEnactResult deliverBolus(final DetailedBolusInfo detailedBolusInfo) { 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 { try {
pump.activity = MainApp.gs(R.string.combo_pump_action_bolusing, detailedBolusInfo.insulin); pump.activity = MainApp.gs(R.string.combo_pump_action_bolusing, detailedBolusInfo.insulin);
MainApp.bus().post(new EventComboPumpUpdateGUI()); 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 = 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) { if (cancelBolus) {
return new PumpEnactResult().success(true).enacted(false); return new PumpEnactResult().success(true).enacted(false);
} }
@ -532,17 +583,18 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
BolusProgressReporter progressReporter = detailedBolusInfo.isSMB ? nullBolusProgressReporter : bolusProgressReporter; BolusProgressReporter progressReporter = detailedBolusInfo.isSMB ? nullBolusProgressReporter : bolusProgressReporter;
// start bolus delivery // start bolus delivery
bolusInProgress = true; scripterIsBolusing = true;
runCommand(null, 0, runCommand(null, 0,
() -> ruffyScripter.deliverBolus(detailedBolusInfo.insulin, progressReporter)); () -> ruffyScripter.deliverBolus(detailedBolusInfo.insulin, progressReporter));
bolusInProgress = false; scripterIsBolusing = false;
// Note that the result of the issued bolus command is not checked. If there was // 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 // a connection problem, ruffyscripter tried to recover and we can just check the
// history below to see what was actually delivered // history below to see what was actually delivered
// get last bolus from pump history for verification // 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) { if (!postBolusStateResult.success) {
return new PumpEnactResult().success(false).enacted(false) return new PumpEnactResult().success(false).enacted(false)
.comment(MainApp.gs(R.string.combo_error_bolus_verification_failed)); .comment(MainApp.gs(R.string.combo_error_bolus_verification_failed));
@ -552,7 +604,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
: null; : null;
// no bolus delivered? // no bolus delivered?
if (lastPumpBolus == null || lastPumpBolus.equals(previousBolus) ) { if (lastPumpBolus == null || lastPumpBolus.equals(previousBolus)) {
if (cancelBolus) { if (cancelBolus) {
return new PumpEnactResult().success(true).enacted(false); return new PumpEnactResult().success(true).enacted(false);
} else { } else {
@ -568,7 +620,9 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
return new PumpEnactResult().success(false).enacted(true) return new PumpEnactResult().success(false).enacted(true)
.comment(MainApp.gs(R.string.combo_error_updating_treatment_record)); .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 // only a partial bolus was delivered
if (Math.abs(lastPumpBolus.amount - detailedBolusInfo.insulin) > 0.01) { 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) { private boolean addBolusToTreatments(DetailedBolusInfo detailedBolusInfo, Bolus lastPumpBolus) {
DetailedBolusInfo dbi = detailedBolusInfo.copy(); DetailedBolusInfo dbi = detailedBolusInfo.copy();
dbi.date = calculateFakeBolusDate(lastPumpBolus); dbi.date = calculateFakeBolusDate(lastPumpBolus);
dbi.pumpId = calculateFakeBolusDate(lastPumpBolus); dbi.pumpId = dbi.date;
dbi.source = Source.PUMP; dbi.source = Source.PUMP;
dbi.insulin = lastPumpBolus.amount; dbi.insulin = lastPumpBolus.amount;
try { try {
@ -610,23 +664,25 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
if (!treatmentCreated) { if (!treatmentCreated) {
log.error("Adding treatment record overrode an existing record: " + dbi); log.error("Adding treatment record overrode an existing record: " + dbi);
if (dbi.isSMB) { 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)); MainApp.bus().post(new EventNewNotification(notification));
} }
Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError") Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION) .putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("bolus", String.valueOf(lastPumpBolus.amount))
.putCustomAttribute("issue", "record with same timestamp existed and was overridden")); .putCustomAttribute("issue", "record with same timestamp existed and was overridden"));
return false; return false;
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Adding treatment record failed", e); log.error("Adding treatment record failed", e);
if (dbi.isSMB) { 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)); MainApp.bus().post(new EventNewNotification(notification));
Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError") Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION) .putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("bolus", String.valueOf(lastPumpBolus.amount))
.putCustomAttribute("issue", "adding record caused exception")); .putCustomAttribute("issue", "adding record caused exception"));
} }
return false; return false;
@ -636,7 +692,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
@Override @Override
public void stopBolusDelivering() { public void stopBolusDelivering() {
if (bolusInProgress) { if (scripterIsBolusing) {
ruffyScripter.cancelBolus(); ruffyScripter.cancelBolus();
} }
cancelBolus = true; cancelBolus = true;
@ -780,9 +836,11 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
CommandResult commandResult; CommandResult commandResult;
try { try {
if (!ruffyScripter.isConnected()) { if (!ruffyScripter.isConnected()) {
String originalActivity = pump.activity;
pump.activity = MainApp.gs(R.string.combo_activity_checking_pump_state); pump.activity = MainApp.gs(R.string.combo_activity_checking_pump_state);
MainApp.bus().post(new EventComboPumpUpdateGUI()); MainApp.bus().post(new EventComboPumpUpdateGUI());
CommandResult preCheckError = runOnConnectChecks(); CommandResult preCheckError = runOnConnectChecks();
pump.activity = originalActivity;
if (preCheckError != null) { if (preCheckError != null) {
updateLocalData(preCheckError); updateLocalData(preCheckError);
return preCheckError; return preCheckError;
@ -855,7 +913,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
// turn benign warnings into notifications // turn benign warnings into notifications
notifyAboutPumpWarning(activeAlert); notifyAboutPumpWarning(activeAlert);
ruffyScripter.confirmAlert(activeAlert.warningCode); ruffyScripter.confirmAlert(activeAlert.warningCode);
} else if (activeAlert.errorCode != null){ } else if (activeAlert.errorCode != null) {
Notification notification = new Notification(); Notification notification = new Notification();
notification.date = new Date(); notification.date = new Date();
notification.id = Notification.COMBO_PUMP_ALARM; notification.id = Notification.COMBO_PUMP_ALARM;
@ -1055,7 +1113,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
for (Bolus pumpBolus : history.bolusHistory) { for (Bolus pumpBolus : history.bolusHistory) {
DetailedBolusInfo dbi = new DetailedBolusInfo(); DetailedBolusInfo dbi = new DetailedBolusInfo();
dbi.date = calculateFakeBolusDate(pumpBolus); dbi.date = calculateFakeBolusDate(pumpBolus);
dbi.pumpId = calculateFakeBolusDate(pumpBolus); dbi.pumpId = dbi.date;
dbi.source = Source.PUMP; dbi.source = Source.PUMP;
dbi.insulin = pumpBolus.amount; dbi.insulin = pumpBolus.amount;
dbi.eventType = CareportalEvent.CORRECTIONBOLUS; 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 /** 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. * 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 * 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. * and most time clashes will be around SMBs which are covered.
*/ */
private long calculateFakeBolusDate(Bolus pumpBolus) { 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 // 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 * @return null on success or the failed command result
*/ */
private CommandResult checkHistory() { private CommandResult checkHistory() {
CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3, ruffyScripter::readQuickInfo); CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3,
if (quickInfoResult.history != null && !quickInfoResult.history.bolusHistory.isEmpty() () -> ruffyScripter.readQuickInfo(2));
&& quickInfoResult.history.bolusHistory.get(0).timestamp == timestampOfLastKnownPumpBolusRecord) {
// 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; return null;
} }
// OPTIMIZE this reads the entire history on start, so this could be optimized by persisting // compare recent records
// `timestampOfLastKnownPumpBolusRecord`, though this should be thought through, to make sure List<Bolus> initialPumpBolusHistory = quickInfoResult.history.bolusHistory;
// all scenarios are covered 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, () -> CommandResult historyResult = runCommand(MainApp.gs(R.string.combo_activity_reading_pump_history), 3, () ->
ruffyScripter.readHistory(new PumpHistoryRequest() ruffyScripter.readHistory(new PumpHistoryRequest().bolusHistory(lastKnownPumpRecordTimestamp)));
.bolusHistory(timestampOfLastKnownPumpBolusRecord)));
if (!historyResult.success) { if (!historyResult.success) {
pumpHistoryChanged = true;
return historyResult; 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()) { pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history);
timestampOfLastKnownPumpBolusRecord = historyResult.history.bolusHistory.get(0).timestamp; 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; return null;

View file

@ -32,7 +32,7 @@ public interface RuffyCommands {
CommandResult readPumpState(); CommandResult readPumpState();
/** Read reservoir level and last bolus via Quick Info */ /** 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 /** Reads pump history via the My Data menu. The {@link PumpHistoryRequest} specifies
* what types of data and how far back data is returned. */ * 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 * operations and are cleanly separated from the thread management, connection management etc
*/ */
public class RuffyScripter implements RuffyCommands { public class RuffyScripter implements RuffyCommands {
private final boolean readQuickInfo = true;
private static final Logger log = LoggerFactory.getLogger(RuffyScripter.class); private static final Logger log = LoggerFactory.getLogger(RuffyScripter.class);
private IRuffyService ruffyService; private IRuffyService ruffyService;
@ -225,17 +223,11 @@ public class RuffyScripter implements RuffyCommands {
} }
@Override @Override
public CommandResult readQuickInfo() { public CommandResult readQuickInfo(int numberOfBolusRecordsToRetrieve) {
if (readQuickInfo) { Answers.getInstance().logCustom(new CustomEvent("ComboReadQuickInfoCmd")
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("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION)); .putCustomAttribute("version", BuildConfig.VERSION));
return runCommand(new ReadHistoryCommand(new PumpHistoryRequest().bolusHistory(PumpHistoryRequest.LAST))); return runCommand(new ReadQuickInfoCommand(numberOfBolusRecordsToRetrieve));
} }
public void returnToRootMenu() { public void returnToRootMenu() {
@ -436,7 +428,7 @@ public class RuffyScripter implements RuffyCommands {
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromConnectionLoss") Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromConnectionLoss")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION) .putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("activeCommand", "" + activeCmd) .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
.putCustomAttribute("success", connected ? "true" : "else")); .putCustomAttribute("success", connected ? "true" : "else"));
return connected; return connected;
} }
@ -451,7 +443,8 @@ public class RuffyScripter implements RuffyCommands {
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION) .putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("activeCommand", "" + activeCmd) .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
.putCustomAttribute("exit", "1")
.putCustomAttribute("success", "false")); .putCustomAttribute("success", "false"));
return new PumpState(); return new PumpState();
} }
@ -469,15 +462,18 @@ public class RuffyScripter implements RuffyCommands {
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION) .putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("activeCommand", "" + activeCmd) .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
.putCustomAttribute("exit", "2")
.putCustomAttribute("success", "true")); .putCustomAttribute("success", "true"));
return pumpState; return pumpState;
} catch (Exception e) { } catch (Exception e) {
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION) .putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("activeCommand", "" + activeCmd) .putCustomAttribute("exit", "3")
.putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
.putCustomAttribute("success", "false")); .putCustomAttribute("success", "false"));
log.debug("Reading pump state during recovery failed", e); log.debug("Reading pump state during recovery failed", e);
return new PumpState(); return new PumpState();
} }
@ -502,7 +498,7 @@ public class RuffyScripter implements RuffyCommands {
Answers.getInstance().logCustom(new CustomEvent("ComboConnectTimeout") Answers.getInstance().logCustom(new CustomEvent("ComboConnectTimeout")
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
.putCustomAttribute("version", BuildConfig.VERSION) .putCustomAttribute("version", BuildConfig.VERSION)
.putCustomAttribute("activeCommand", "" + activeCmd) .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
.putCustomAttribute("previousCommand", previousCommand)); .putCustomAttribute("previousCommand", previousCommand));
throw new CommandException("Timeout connecting to pump"); 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.MenuAttribute;
import org.monkey.d.ruffy.ruffy.driver.display.MenuType; 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.MenuDate;
import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime; import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -145,7 +144,7 @@ public class ReadHistoryCommand extends BaseCommand {
int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
while (true) { while (true) {
Tdd tdd = readTddRecord(); Tdd tdd = readTddRecord();
if (requestedTime != PumpHistoryRequest.FULL && tdd.timestamp <= requestedTime) { if (requestedTime != PumpHistoryRequest.FULL && tdd.timestamp < requestedTime) {
break; break;
} }
log.debug("Read TDD record #" + record + "/" + totalRecords); 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); int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
while (true) { while (true) {
Tbr tbr = readTbrRecord(); Tbr tbr = readTbrRecord();
if (requestedTime != PumpHistoryRequest.FULL && tbr.timestamp <= requestedTime) { if (requestedTime != PumpHistoryRequest.FULL && tbr.timestamp < requestedTime) {
break; break;
} }
log.debug("Read TBR record #" + record + "/" + totalRecords); 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); int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
while (true) { while (true) {
Bolus bolus = readBolusRecord(); Bolus bolus = readBolusRecord();
if (requestedTime != PumpHistoryRequest.FULL && bolus.timestamp <= requestedTime) { if (requestedTime != PumpHistoryRequest.FULL && bolus.timestamp < requestedTime) {
break; break;
} }
log.debug("Read bolus record #" + record + "/" + totalRecords); 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); int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
while (true) { while (true) {
PumpAlert error = readAlertRecord(); PumpAlert error = readAlertRecord();
if (requestedTime != PumpHistoryRequest.FULL && error.timestamp <= requestedTime) { if (requestedTime != PumpHistoryRequest.FULL && error.timestamp < requestedTime) {
break; break;
} }
log.debug("Read alert record #" + record + "/" + totalRecords); 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.MenuAttribute;
import org.monkey.d.ruffy.ruffy.driver.display.MenuType; import org.monkey.d.ruffy.ruffy.driver.display.MenuType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus;
import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistory; import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistory;
public class ReadQuickInfoCommand extends BaseCommand { 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 @Override
public void execute() { public void execute() {
scripter.verifyRootMenuIsDisplayed(); scripter.verifyRootMenuIsDisplayed();
// navigate to reservoir menu
scripter.pressCheckKey(); scripter.pressCheckKey();
scripter.waitForMenuToBeLeft(MenuType.MAIN_MENU); scripter.waitForMenuToBeLeft(MenuType.MAIN_MENU);
scripter.waitForMenuToBeLeft(MenuType.STOP); scripter.waitForMenuToBeLeft(MenuType.STOP);
scripter.verifyMenuIsDisplayed(MenuType.QUICK_INFO); scripter.verifyMenuIsDisplayed(MenuType.QUICK_INFO);
result.reservoirLevel = ((Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.REMAINING_INSULIN)).intValue(); result.reservoirLevel = ((Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.REMAINING_INSULIN)).intValue();
scripter.pressCheckKey(); if (numberOfBolusRecordsToRetrieve > 0) {
List<Bolus> bolusHistory = new ArrayList<>(1); // navigate to bolus data menu
bolusHistory.add(readBolusRecord()); scripter.pressCheckKey();
result.history = new PumpHistory().bolusHistory(bolusHistory); 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(); scripter.returnToRootMenu();
result.success = true; result.success = true;
} }

View file

@ -5,8 +5,10 @@ import java.util.Date;
/** What data a 'read history' request should return. */ /** What data a 'read history' request should return. */
public class PumpHistoryRequest { public class PumpHistoryRequest {
/* History to read: /* History to read:
Either the timestamp of the last known record to fetch all newer records, Either the timestamp of the last known record or one of the constants to read no history
or one of the constants to read no history or all of it. 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 LAST = -2;
public static final long SKIP = -1; 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_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_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_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> </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);
}
}