diff --git a/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRTHandler.aidl b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRTHandler.aidl new file mode 100644 index 0000000000..f17512b8da --- /dev/null +++ b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRTHandler.aidl @@ -0,0 +1,20 @@ +// IRTHandler.aidl +package org.monkey.d.ruffy.ruffy.driver; + +// Declare any non-default types here with import statements +import org.monkey.d.ruffy.ruffy.driver.display.Menu; + +interface IRTHandler { + void log(String message); + void fail(String message); + + void requestBluetooth(); + void rtStopped(); + void rtStarted(); + + void rtClearDisplay(); + void rtUpdateDisplay(in byte[] quarter, int which); + + void rtDisplayHandleMenu(in Menu menu); + void rtDisplayHandleNoMenu(); +} diff --git a/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRuffyService.aidl b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRuffyService.aidl new file mode 100644 index 0000000000..6c988aa038 --- /dev/null +++ b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/IRuffyService.aidl @@ -0,0 +1,23 @@ +// IRuffyService.aidl +package org.monkey.d.ruffy.ruffy.driver; + +// Declare any non-default types here with import statements +import org.monkey.d.ruffy.ruffy.driver.IRTHandler; + +interface IRuffyService { + + void setHandler(IRTHandler handler); + + /** Connect to the pump + * + * @return 0 if successful, -1 otherwise + */ + int doRTConnect(); + + /** Disconnect from the pump */ + void doRTDisconnect(); + + void rtSendKey(byte keyCode, boolean changed); + void resetPairing(); + boolean isConnected(); +} diff --git a/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/display/Menu.aidl b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/display/Menu.aidl new file mode 100644 index 0000000000..f0b99ec918 --- /dev/null +++ b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/display/Menu.aidl @@ -0,0 +1,3 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +parcelable Menu; \ No newline at end of file diff --git a/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/package-info.java b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/package-info.java new file mode 100644 index 0000000000..ab1def7407 --- /dev/null +++ b/app/src/main/aidl/org/monkey/d/ruffy/ruffy/driver/package-info.java @@ -0,0 +1 @@ +//b916a900c0899ef58ad58c7427d1c30d3c8731f4 \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/Config.java b/app/src/main/java/info/nightscout/androidaps/Config.java index 6775b8dac6..a2bf64b7ae 100644 --- a/app/src/main/java/info/nightscout/androidaps/Config.java +++ b/app/src/main/java/info/nightscout/androidaps/Config.java @@ -13,7 +13,7 @@ public class Config { public static final boolean G5UPLOADER = BuildConfig.G5UPLOADER; public static final boolean PUMPCONTROL = BuildConfig.PUMPCONTROL; - public static final boolean DANAR = BuildConfig.PUMPDRIVERS; + public static final boolean HWPUMPS = BuildConfig.PUMPDRIVERS; public static final boolean ACTION = !BuildConfig.NSCLIENTOLNY && !BuildConfig.G5UPLOADER; public static final boolean VIRTUALPUMP = !BuildConfig.NSCLIENTOLNY && !BuildConfig.G5UPLOADER; @@ -44,4 +44,6 @@ public class Config { public static final boolean logDanaBTComm = true; public static boolean logDanaMessageDetail = true; public static final boolean logDanaSerialEngine = true; + + public static final boolean enableComboBetaFeatures = false; } diff --git a/app/src/main/java/info/nightscout/androidaps/Constants.java b/app/src/main/java/info/nightscout/androidaps/Constants.java index d933018790..c1e6f86131 100644 --- a/app/src/main/java/info/nightscout/androidaps/Constants.java +++ b/app/src/main/java/info/nightscout/androidaps/Constants.java @@ -1,7 +1,5 @@ package info.nightscout.androidaps; -import com.j256.ormlite.stmt.query.In; - /** * Created by mike on 07.06.2016. */ diff --git a/app/src/main/java/info/nightscout/androidaps/MainActivity.java b/app/src/main/java/info/nightscout/androidaps/MainActivity.java index 4a02bda1ae..0ae1e48775 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/MainActivity.java @@ -19,6 +19,9 @@ import android.support.v4.view.ViewPager; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.PopupMenu; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; @@ -27,6 +30,7 @@ import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ImageButton; +import android.widget.TextView; import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.fonts.FontAwesomeModule; @@ -402,10 +406,14 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe message += MainApp.sResources.getString(R.string.configbuilder_nightscoutversion_label) + " " + ConfigBuilderPlugin.nightscoutVersionName; if (MainApp.engineeringMode) message += "\n" + MainApp.gs(R.string.engineering_mode_enabled); - builder.setMessage(message); + message += getString(R.string.about_link_urls); + final SpannableString messageSpanned = new SpannableString(message); + Linkify.addLinks(messageSpanned, Linkify.WEB_URLS); + builder.setMessage(messageSpanned); builder.setPositiveButton(MainApp.sResources.getString(R.string.ok), null); AlertDialog alertDialog = builder.create(); alertDialog.show(); + ((TextView)alertDialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); break; case R.id.nav_exit: log.debug("Exiting"); diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index d9473b3575..5087fefefa 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -54,6 +54,7 @@ import info.nightscout.androidaps.plugins.Persistentnotification.PersistentNotif import info.nightscout.androidaps.plugins.ProfileLocal.LocalProfilePlugin; import info.nightscout.androidaps.plugins.ProfileNS.NSProfilePlugin; import info.nightscout.androidaps.plugins.ProfileSimple.SimpleProfilePlugin; +import info.nightscout.androidaps.plugins.PumpCombo.ComboPlugin; import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPlugin; import info.nightscout.androidaps.plugins.PumpDanaRKorean.DanaRKoreanPlugin; import info.nightscout.androidaps.plugins.PumpDanaRS.DanaRSPlugin; @@ -78,6 +79,7 @@ import info.nightscout.androidaps.receivers.KeepAliveReceiver; import info.nightscout.androidaps.receivers.NSAlarmReceiver; import info.nightscout.utils.FabricPrivacy; import info.nightscout.utils.NSUpload; +import info.nightscout.utils.SP; import io.fabric.sdk.android.Fabric; @@ -123,6 +125,12 @@ public class MainApp extends Application { log.info("Version: " + BuildConfig.VERSION_NAME); log.info("BuildVersion: " + BuildConfig.BUILDVERSION); + String extFilesDir = this.getLogDirectory(); + File engineeringModeSemaphore = new File(extFilesDir,"engineering_mode"); + + engineeringMode = engineeringModeSemaphore.exists() && engineeringModeSemaphore.isFile(); + devBranch = BuildConfig.VERSION.contains("dev"); + sBus = Config.logEvents ? new LoggingBus(ThreadEnforcer.ANY) : new Bus(ThreadEnforcer.ANY); registerLocalBroadcastReceiver(); @@ -141,12 +149,13 @@ public class MainApp extends Application { pluginsList.add(SensitivityOref0Plugin.getPlugin()); pluginsList.add(SensitivityAAPSPlugin.getPlugin()); pluginsList.add(SensitivityWeightedAveragePlugin.getPlugin()); - if (Config.DANAR) pluginsList.add(DanaRPlugin.getPlugin()); - if (Config.DANAR) pluginsList.add(DanaRKoreanPlugin.getPlugin()); - if (Config.DANAR) pluginsList.add(DanaRv2Plugin.getPlugin()); - if (Config.DANAR) pluginsList.add(DanaRSPlugin.getPlugin()); + if (Config.HWPUMPS) pluginsList.add(DanaRPlugin.getPlugin()); + if (Config.HWPUMPS) pluginsList.add(DanaRKoreanPlugin.getPlugin()); + if (Config.HWPUMPS) pluginsList.add(DanaRv2Plugin.getPlugin()); + if (Config.HWPUMPS) pluginsList.add(DanaRSPlugin.getPlugin()); pluginsList.add(CareportalPlugin.getPlugin()); - if (Config.DANAR && engineeringMode) pluginsList.add(InsightPumpPlugin.getPlugin()); // <-- Enable Insight plugin here + if (Config.HWPUMPS && engineeringMode) pluginsList.add(InsightPumpPlugin.getPlugin()); // <-- Enable Insight plugin here + if (Config.HWPUMPS && engineeringMode) pluginsList.add(ComboPlugin.getPlugin()); // <-- Enable Combo plugin here if (Config.MDI) pluginsList.add(MDIPlugin.getPlugin()); if (Config.VIRTUALPUMP) pluginsList.add(VirtualPumpPlugin.getPlugin()); if (Config.APS) pluginsList.add(LoopPlugin.getPlugin()); @@ -202,12 +211,6 @@ public class MainApp extends Application { } }).start(); - String extFilesDir = this.getLogDirectory(); - File engineeringModeSemaphore = new File(extFilesDir,"engineering_mode"); - - engineeringMode = engineeringModeSemaphore.exists() && engineeringModeSemaphore.isFile(); - devBranch = BuildConfig.VERSION.contains("dev"); - if (!isEngineeringModeOrRelease()) { Notification n = new Notification(Notification.TOAST_ALARM, gs(R.string.closed_loop_disabled_on_dev_branch), Notification.NORMAL); bus().post(new EventNewNotification(n)); diff --git a/app/src/main/java/info/nightscout/androidaps/PreferencesActivity.java b/app/src/main/java/info/nightscout/androidaps/PreferencesActivity.java index 4ea3a80d3a..0f712c787f 100644 --- a/app/src/main/java/info/nightscout/androidaps/PreferencesActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/PreferencesActivity.java @@ -1,11 +1,9 @@ package info.nightscout.androidaps; -import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.EditTextPreference; import android.preference.ListPreference; -import android.preference.MultiSelectListPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; @@ -155,7 +153,7 @@ public class PreferencesActivity extends PreferenceActivity implements SharedPre addPreferencesFromResourceIfEnabled(SensitivityWeightedAveragePlugin.getPlugin(), PluginBase.SENSITIVITY); addPreferencesFromResourceIfEnabled(SensitivityOref0Plugin.getPlugin(), PluginBase.SENSITIVITY); - if (Config.DANAR) { + if (Config.HWPUMPS) { addPreferencesFromResourceIfEnabled(DanaRPlugin.getPlugin(), PluginBase.PUMP); addPreferencesFromResourceIfEnabled(DanaRKoreanPlugin.getPlugin(), PluginBase.PUMP); addPreferencesFromResourceIfEnabled(DanaRv2Plugin.getPlugin(), PluginBase.PUMP); diff --git a/app/src/main/java/info/nightscout/androidaps/data/DetailedBolusInfo.java b/app/src/main/java/info/nightscout/androidaps/data/DetailedBolusInfo.java index 7d1dcac9ad..d8c0bea28a 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/DetailedBolusInfo.java +++ b/app/src/main/java/info/nightscout/androidaps/data/DetailedBolusInfo.java @@ -6,10 +6,8 @@ import org.json.JSONObject; import java.util.Date; -import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.db.CareportalEvent; import info.nightscout.androidaps.db.Source; -import info.nightscout.androidaps.interfaces.InsulinInterface; /** * Created by mike on 29.05.2017. @@ -31,6 +29,24 @@ public class DetailedBolusInfo { public boolean isSMB = false; // is a Super-MicroBolus public long deliverAt = 0; // SMB should be delivered within 1 min from this time + public DetailedBolusInfo copy() { + DetailedBolusInfo copy = new DetailedBolusInfo(); + copy.date = this.date; + copy.eventType = this.eventType; + copy.insulin = this.insulin; + copy.carbs = this.carbs; + copy.source = this.source; + copy.isValid = this.isValid; + copy.glucose = this.glucose; + copy.glucoseType = this.glucoseType; + copy.carbTime = this.carbTime; + copy.boluscalc = this.boluscalc; + copy.context = this.context; + copy.pumpId = this.pumpId; + copy.isSMB = this.isSMB; + return copy; + } + @Override public String toString() { return new Date(date).toLocaleString() + diff --git a/app/src/main/java/info/nightscout/androidaps/data/PumpEnactResult.java b/app/src/main/java/info/nightscout/androidaps/data/PumpEnactResult.java index 4732c20fd3..9aa09244aa 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/PumpEnactResult.java +++ b/app/src/main/java/info/nightscout/androidaps/data/PumpEnactResult.java @@ -13,7 +13,7 @@ import info.nightscout.androidaps.R; import info.nightscout.utils.DecimalFormatter; import info.nightscout.utils.Round; -public class PumpEnactResult extends Object { +public class PumpEnactResult { private static Logger log = LoggerFactory.getLogger(PumpEnactResult.class); public boolean success = false; // request was processed successfully (but possible no change was needed) @@ -61,6 +61,7 @@ public class PumpEnactResult extends Object { this.percent = percent; return this; } + public PumpEnactResult isPercent(boolean isPercent) { this.isPercent = isPercent; return this; diff --git a/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java b/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java index 64c807f8dc..45666cf4ae 100644 --- a/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java +++ b/app/src/main/java/info/nightscout/androidaps/interfaces/PumpInterface.java @@ -5,8 +5,8 @@ import org.json.JSONObject; import java.util.Date; import info.nightscout.androidaps.data.DetailedBolusInfo; -import info.nightscout.androidaps.data.PumpEnactResult; import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; /** * Created by mike on 04.06.2016. diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/ConfigBuilder/ConfigBuilderPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/ConfigBuilder/ConfigBuilderPlugin.java index 45e4ddffc6..e0a8155ffa 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/ConfigBuilder/ConfigBuilderPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/ConfigBuilder/ConfigBuilderPlugin.java @@ -42,6 +42,7 @@ import info.nightscout.androidaps.interfaces.SensitivityInterface; import info.nightscout.androidaps.interfaces.TreatmentsInterface; import info.nightscout.androidaps.plugins.Loop.APSResult; import info.nightscout.androidaps.plugins.Loop.LoopPlugin; +import info.nightscout.androidaps.plugins.Overview.events.EventDismissNotification; import info.nightscout.androidaps.plugins.Overview.events.EventNewNotification; import info.nightscout.androidaps.plugins.Overview.notifications.Notification; import info.nightscout.androidaps.plugins.PumpVirtual.VirtualPumpPlugin; @@ -738,6 +739,7 @@ public class ConfigBuilderPlugin implements PluginBase, ConstraintsInterface, Tr @Override public void addToHistoryProfileSwitch(ProfileSwitch profileSwitch) { + MainApp.bus().post(new EventDismissNotification(Notification.PROFILE_SWITCH_MISSING)); activeTreatments.addToHistoryProfileSwitch(profileSwitch); NSUpload.uploadProfileSwitch(profileSwitch); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java index 5d9a652816..e90a77c524 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java @@ -1241,9 +1241,7 @@ public class OverviewFragment extends Fragment implements View.OnClickListener, flag &= ~Paint.STRIKE_THRU_TEXT_FLAG; bgView.setPaintFlags(flag); - Long agoMsec = System.currentTimeMillis() - lastBG.date; - int agoMin = (int) (agoMsec / 60d / 1000d); - timeAgoView.setText(String.format(MainApp.sResources.getString(R.string.minago), agoMin)); + timeAgoView.setText(DateUtil.minAgo(lastBG.date)); // iob MainApp.getConfigBuilder().updateTotalIOBTreatments(); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/notifications/Notification.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/notifications/Notification.java index 5868ed316a..787545965c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/notifications/Notification.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/notifications/Notification.java @@ -50,13 +50,15 @@ public class Notification { public static final int TOAST_ALARM = 22; public static final int WRONGBASALSTEP = 23; public static final int WRONG_DRIVER = 24; + public static final int COMBO_PUMP_ALARM = 25; public static final int PUMP_UNREACHABLE = 26; public static final int BG_READINGS_MISSED = 27; public static final int UNSUPPORTED_FIRMWARE = 28; public static final int MINIMAL_BASAL_VALUE_REPLACED = 29; public static final int BASAL_PROFILE_NOT_ALIGNED_TO_HOURS = 30; public static final int ZERO_VALUE_IN_PROFILE = 31; - public static final int NOT_ENG_MODE_OR_RELEASE = 32; + public static final int PROFILE_SWITCH_MISSING = 32; + public static final int NOT_ENG_MODE_OR_RELEASE = 33; public int id; public Date date; @@ -201,7 +203,7 @@ public class Notification { //log.debug("OpenAPS Alerts enabled: "+openAPSEnabledAlerts); // if no thresshold from Ns get it loccally if(threshold == null) threshold = SP.getDouble(R.string.key_nsalarm_staledatavalue,15D); - // No threshold of OpenAPS Alarm so using the one for BG + // No threshold of OpenAPS Alarm so using the one for BG // Added OpenAPSEnabledAlerts to alarm check if((bgReadingAgoMin > threshold && SP.getBoolean(R.string.key_nsalarm_staledata, false))||(bgReadingAgoMin > threshold && openAPSEnabledAlerts)){ return true; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboAlertHistoryDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboAlertHistoryDialog.java new file mode 100644 index 0000000000..6f30b3aed9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboAlertHistoryDialog.java @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.plugins.PumpCombo; + +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.text.DateFormat; +import java.util.List; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpAlert; +import info.nightscout.androidaps.R; + +public class ComboAlertHistoryDialog extends DialogFragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.combo_alert_history_fragment, container, false); + TextView text = (TextView) layout.findViewById(R.id.combo_error_history_text); + List errors = ComboPlugin.getPlugin().getPump().errorHistory; + if (errors.isEmpty()) { + text.setText(R.string.combo_no_alert_data_note); + } else { + StringBuilder sb = new StringBuilder(); + DateFormat dateTimeFormatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + boolean first = true; + for (PumpAlert error : errors) { + if (first) { + first = false; + } else { + sb.append("\n"); + } + sb.append(dateTimeFormatter.format(error.timestamp)); + sb.append(" "); + sb.append(error.message); + if (error.warningCode != null) { + sb.append(" (W"); + sb.append(error.warningCode); + sb.append(")"); + } + if (error.errorCode != null) { + sb.append(" (E"); + sb.append(error.errorCode); + sb.append(")"); + } + } + text.setText(sb.toString()); + } + return layout; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboFragment.java new file mode 100644 index 0000000000..d7c0a4b7c6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboFragment.java @@ -0,0 +1,312 @@ +package info.nightscout.androidaps.plugins.PumpCombo; + + +import android.app.Activity; +import android.app.AlertDialog; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import com.squareup.otto.Subscribe; + +import org.apache.commons.lang3.StringUtils; + +import info.nightscout.androidaps.Config; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpState; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.Common.SubscriberFragment; +import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.PumpCombo.events.EventComboPumpUpdateGUI; +import info.nightscout.androidaps.queue.Callback; +import info.nightscout.androidaps.queue.events.EventQueueChanged; +import info.nightscout.utils.DateUtil; +import info.nightscout.utils.SP; + +public class ComboFragment extends SubscriberFragment implements View.OnClickListener, View.OnLongClickListener { + private TextView stateView; + private TextView activityView; + private TextView batteryView; + private TextView reservoirView; + private TextView lastConnectionView; + private TextView lastBolusView; + private TextView baseBasalRate; + private TextView tempBasalText; + private Button refreshButton; + private Button alertsButton; + private Button tddsButton; + private TextView bolusCount; + private TextView tbrCount; + private Button fullHistoryButton; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.combopump_fragment, container, false); + + stateView = (TextView) view.findViewById(R.id.combo_state); + activityView = (TextView) view.findViewById(R.id.combo_activity); + batteryView = (TextView) view.findViewById(R.id.combo_pumpstate_battery); + reservoirView = (TextView) view.findViewById(R.id.combo_insulinstate); + lastBolusView = (TextView) view.findViewById(R.id.combo_last_bolus); + lastConnectionView = (TextView) view.findViewById(R.id.combo_lastconnection); + baseBasalRate = (TextView) view.findViewById(R.id.combo_base_basal_rate); + tempBasalText = (TextView) view.findViewById(R.id.combo_temp_basal); + bolusCount = (TextView) view.findViewById(R.id.combo_bolus_count); + tbrCount = (TextView) view.findViewById(R.id.combo_tbr_count); + + refreshButton = (Button) view.findViewById(R.id.combo_refresh_button); + refreshButton.setOnClickListener(this); + + alertsButton = (Button) view.findViewById(R.id.combo_alerts_button); + alertsButton.setOnClickListener(this); + alertsButton.setOnLongClickListener(this); + + tddsButton = (Button) view.findViewById(R.id.combo_tdds_button); + tddsButton.setOnClickListener(this); + tddsButton.setOnLongClickListener(this); + + fullHistoryButton = (Button) view.findViewById(R.id.combo_full_history_button); + fullHistoryButton.setOnClickListener(this); + fullHistoryButton.setOnLongClickListener(this); + + updateGUI(); + return view; + } + + private void runOnUiThread(Runnable action) { + Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(action); + } + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.combo_refresh_button: + refreshButton.setEnabled(false); + ConfigBuilderPlugin.getCommandQueue().readStatus("User request", new Callback() { + @Override + public void run() { + runOnUiThread(() -> refreshButton.setEnabled(true)); + } + }); + break; + case R.id.combo_alerts_button: + ComboAlertHistoryDialog ehd = new ComboAlertHistoryDialog(); + ehd.show(getFragmentManager(), ComboAlertHistoryDialog.class.getSimpleName()); + break; + case R.id.combo_tdds_button: + ComboTddHistoryDialog thd = new ComboTddHistoryDialog(); + thd.show(getFragmentManager(), ComboTddHistoryDialog.class.getSimpleName()); + break; + case R.id.combo_full_history_button: + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setMessage(R.string.combo_read_full_history_info); + builder.show(); + break; + } + } + + // TODO clean up when when queuing + @Override + public boolean onLongClick(View view) { + switch (view.getId()) { + case R.id.combo_alerts_button: + alertsButton.setEnabled(false); + tddsButton.setEnabled(false); + fullHistoryButton.setEnabled(false); + new Thread(() -> ComboPlugin.getPlugin().readAlertData(new Callback() { + @Override + public void run() { + runOnUiThread(() -> { + alertsButton.setEnabled(true); + tddsButton.setEnabled(true); + fullHistoryButton.setEnabled(true); + }); + } + })).start(); + return true; + case R.id.combo_tdds_button: + alertsButton.setEnabled(false); + tddsButton.setEnabled(false); + fullHistoryButton.setEnabled(false); + new Thread(() -> ComboPlugin.getPlugin().readTddData(new Callback() { + @Override + public void run() { + runOnUiThread(() -> { + alertsButton.setEnabled(true); + tddsButton.setEnabled(true); + fullHistoryButton.setEnabled(true); + }); + } + })).start(); + return true; + case R.id.combo_full_history_button: + alertsButton.setEnabled(false); + tddsButton.setEnabled(false); + fullHistoryButton.setEnabled(false); + new Thread(() -> ComboPlugin.getPlugin().readAllPumpData(new Callback() { + @Override + public void run() { + runOnUiThread(() -> { + alertsButton.setEnabled(true); + tddsButton.setEnabled(true); + fullHistoryButton.setEnabled(true); + }); + } + })).start(); + return true; + } + return false; + } + + @Subscribe + public void onStatusEvent(final EventComboPumpUpdateGUI ignored) { + updateGUI(); + } + + @Subscribe + public void onStatusEvent(final EventQueueChanged ignored) { + updateGUI(); + } + + + public void updateGUI() { + runOnUiThread(() -> { + ComboPlugin plugin = ComboPlugin.getPlugin(); + + // state + stateView.setText(plugin.getStateSummary()); + PumpState ps = plugin.getPump().state; + if (ps.insulinState == PumpState.EMPTY || ps.batteryState == PumpState.EMPTY + || ps.activeAlert != null && ps.activeAlert.errorCode != null) { + stateView.setTextColor(Color.RED); + stateView.setTypeface(null, Typeface.BOLD); + } else if (plugin.getPump().state.suspended + || ps.activeAlert != null && ps.activeAlert.warningCode != null) { + stateView.setTextColor(Color.YELLOW); + stateView.setTypeface(null, Typeface.BOLD); + } else { + stateView.setTextColor(Color.WHITE); + stateView.setTypeface(null, Typeface.NORMAL); + } + + // activity + String activity = plugin.getPump().activity; + if (StringUtils.isNotEmpty(activity)) { + activityView.setTextSize(14); + activityView.setText(activity); + } else { + activityView.setTextSize(20); + activityView.setText("{fa-bed}"); + } + + if (plugin.isInitialized()) { + refreshButton.setVisibility(View.VISIBLE); + if (Config.enableComboBetaFeatures) { + alertsButton.setVisibility(View.VISIBLE); + tddsButton.setVisibility(View.VISIBLE); + } + fullHistoryButton.setVisibility(View.VISIBLE); + + // battery + batteryView.setTextSize(20); + if (ps.batteryState == PumpState.EMPTY) { + batteryView.setText("{fa-battery-empty}"); + batteryView.setTextColor(Color.RED); + } else if (ps.batteryState == PumpState.LOW) { + batteryView.setText("{fa-battery-quarter}"); + batteryView.setTextColor(Color.YELLOW); + } else { + batteryView.setText("{fa-battery-full}"); + batteryView.setTextColor(Color.WHITE); + } + + // reservoir + int reservoirLevel = plugin.getPump().reservoirLevel; + if (reservoirLevel != -1) { + reservoirView.setText(reservoirLevel + " " + MainApp.sResources.getString(R.string.treatments_wizard_unit_label)); + } else if (ps.insulinState == PumpState.LOW) { + reservoirView.setText(MainApp.gs(R.string.combo_reservoir_low)); + } else if (ps.insulinState == PumpState.EMPTY) { + reservoirView.setText(MainApp.gs(R.string.combo_reservoir_empty)); + } else { + reservoirView.setText(MainApp.gs(R.string.combo_reservoir_normal)); + } + + if (ps.insulinState == PumpState.UNKNOWN) { + reservoirView.setTextColor(Color.WHITE); + reservoirView.setTypeface(null, Typeface.NORMAL); + } else if (ps.insulinState == PumpState.LOW) { + reservoirView.setTextColor(Color.YELLOW); + reservoirView.setTypeface(null, Typeface.BOLD); + } else if (ps.insulinState == PumpState.EMPTY) { + reservoirView.setTextColor(Color.RED); + reservoirView.setTypeface(null, Typeface.BOLD); + } else { + reservoirView.setTextColor(Color.WHITE); + reservoirView.setTypeface(null, Typeface.NORMAL); + } + + // last connection + String minAgo = DateUtil.minAgo(plugin.getPump().lastSuccessfulCmdTime); + long min = (System.currentTimeMillis() - plugin.getPump().lastSuccessfulCmdTime) / 1000 / 60; + if (plugin.getPump().lastSuccessfulCmdTime + 60 * 1000 > System.currentTimeMillis()) { + lastConnectionView.setText(R.string.combo_pump_connected_now); + lastConnectionView.setTextColor(Color.WHITE); + } else if (plugin.getPump().lastSuccessfulCmdTime + 30 * 60 * 1000 < System.currentTimeMillis()) { + lastConnectionView.setText(MainApp.gs(R.string.combo_no_pump_connection, min)); + lastConnectionView.setTextColor(Color.RED); + } else { + lastConnectionView.setText(minAgo); + lastConnectionView.setTextColor(Color.WHITE); + } + + // last bolus + Bolus bolus = plugin.getPump().lastBolus; + if (bolus != null) { + long agoMsc = System.currentTimeMillis() - bolus.timestamp; + double bolusMinAgo = agoMsc / 60d / 1000d; + String unit = MainApp.gs(R.string.treatments_wizard_unit_label); + String ago; + if ((agoMsc < 60 * 1000)) { + ago = MainApp.gs(R.string.combo_pump_connected_now); + } else if (bolusMinAgo < 60) { + ago = DateUtil.minAgo(bolus.timestamp); + } else { + ago = DateUtil.hourAgo(bolus.timestamp); + } + lastBolusView.setText(MainApp.gs(R.string.combo_last_bolus, bolus.amount, unit, ago)); + } else { + lastBolusView.setText(""); + } + + // base basal rate + baseBasalRate.setText(MainApp.gs(R.string.pump_basebasalrate, plugin.getBaseBasalRate())); + + // TBR + String tbrStr = ""; + if (ps.tbrPercent != -1 && ps.tbrPercent != 100) { + long minSinceRead = (System.currentTimeMillis() - plugin.getPump().state.timestamp) / 1000 / 60; + long remaining = ps.tbrRemainingDuration - minSinceRead; + if (remaining >= 0) { + tbrStr = MainApp.gs(R.string.combo_tbr_remaining, ps.tbrPercent, remaining); + } + } + tempBasalText.setText(tbrStr); + + // stats + bolusCount.setText(String.valueOf(SP.getLong(ComboPlugin.COMBO_BOLUSES_DELIVERED, 0L))); + tbrCount.setText(String.valueOf(SP.getLong(ComboPlugin.COMBO_TBRS_SET, 0L))); + } + }); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..ee3a32ad7e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPlugin.java @@ -0,0 +1,1458 @@ +package info.nightscout.androidaps.plugins.PumpCombo; + +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.crashlytics.android.answers.Answers; +import com.crashlytics.android.answers.CustomEvent; + +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; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.CareportalEvent; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TemporaryBasal; +import info.nightscout.androidaps.db.Treatment; +import info.nightscout.androidaps.events.EventInitializationChanged; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.ConstraintsInterface; +import info.nightscout.androidaps.interfaces.PluginBase; +import info.nightscout.androidaps.interfaces.PumpDescription; +import info.nightscout.androidaps.interfaces.PumpInterface; +import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.Overview.events.EventDismissNotification; +import info.nightscout.androidaps.plugins.Overview.events.EventNewNotification; +import info.nightscout.androidaps.plugins.Overview.events.EventOverviewBolusProgress; +import info.nightscout.androidaps.plugins.Overview.notifications.Notification; +import info.nightscout.androidaps.plugins.PumpCombo.events.EventComboPumpUpdateGUI; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BasalProfile; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BolusProgressReporter; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.CommandResult; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpState; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpWarningCodes; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.RuffyCommands; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.RuffyScripter; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.WarningOrErrorCode; +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.PumpHistoryRequest; +import info.nightscout.androidaps.queue.Callback; +import info.nightscout.androidaps.queue.CommandQueue; +import info.nightscout.utils.DateUtil; +import info.nightscout.utils.FabricPrivacy; +import info.nightscout.utils.SP; + +/** + * Created by mike on 05.08.2016. + */ +public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterface { + private static final Logger log = LoggerFactory.getLogger(ComboPlugin.class); + public static final String COMBO_TBRS_SET = "combo_tbrs_set"; + public static final String COMBO_BOLUSES_DELIVERED = "combo_boluses_delivered"; + + private static ComboPlugin plugin = null; + private boolean fragmentEnabled = false; + private boolean fragmentVisible = false; + + private final static PumpDescription pumpDescription = new PumpDescription(); + + static { + // these properties can't be changed on the pump, some via desktop configuration software + pumpDescription.isBolusCapable = true; + pumpDescription.bolusStep = 0.1d; + + pumpDescription.isExtendedBolusCapable = false; + pumpDescription.extendedBolusStep = 0.1d; + pumpDescription.extendedBolusDurationStep = 15; + pumpDescription.extendedBolusMaxDuration = 12 * 60; + + pumpDescription.isTempBasalCapable = true; + pumpDescription.tempBasalStyle = PumpDescription.PERCENT; + + pumpDescription.maxTempPercent = 500; + pumpDescription.tempPercentStep = 10; + + pumpDescription.tempDurationStep = 15; + pumpDescription.tempDurationStep15mAllowed = true; + pumpDescription.tempDurationStep30mAllowed = true; + pumpDescription.tempMaxDuration = 24 * 60; + + + pumpDescription.isSetBasalProfileCapable = true; + pumpDescription.basalStep = 0.01d; + pumpDescription.basalMinimumRate = 0.05d; + + pumpDescription.isRefillingCapable = true; + + pumpDescription.storesCarbInfo = false; + + pumpDescription.is30minBasalRatesCapable = false; + } + + @NonNull + private final RuffyCommands ruffyScripter; + + @NonNull + private static final ComboPump pump = new ComboPump(); + + /** 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; + + /** + * 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. Newest record is the first one. */ + private volatile List recentBoluses = new ArrayList<>(0); + + public static ComboPlugin getPlugin() { + if (plugin == null) + plugin = new ComboPlugin(); + return plugin; + } + + private static final PumpEnactResult OPERATION_NOT_SUPPORTED = new PumpEnactResult() + .success(false).enacted(false).comment(MainApp.gs(R.string.combo_pump_unsupported_operation)); + + private ComboPlugin() { + ruffyScripter = new RuffyScripter(MainApp.instance().getApplicationContext()); + } + + public ComboPump getPump() { + return pump; + } + + @Override + public String getFragmentClass() { + return ComboFragment.class.getName(); + } + + @Override + public String getName() { + return MainApp.gs(R.string.combopump); + } + + @Override + public String getNameShort() { + String name = MainApp.gs(R.string.combopump_shortname); + if (!name.trim().isEmpty()) { + //only if translation exists + return name; + } + // use long name as fallback + return getName(); + } + + String getStateSummary() { + PumpState ps = pump.state; + if (!pump.initialized) { + return MainApp.gs(R.string.combo_pump_state_initializing); + } else if (!validBasalRateProfileSelectedOnPump) { + return MainApp.gs(R.string.loopdisabled); + } else if (ps.activeAlert != null) { + return ps.activeAlert.errorCode != null + ? "E" + ps.activeAlert.errorCode + ": " + ps.activeAlert.message + : "W" + ps.activeAlert.warningCode + ": " + ps.activeAlert.message; + } else if (ps.suspended && (ps.batteryState == PumpState.EMPTY || ps.insulinState == PumpState.EMPTY)) + return MainApp.gs(R.string.combo_pump_state_suspended_due_to_error); + else if (ps.suspended) + return MainApp.gs(R.string.combo_pump_state_suspended_by_user); + return MainApp.gs(R.string.combo_pump_state_running); + } + + @Override + public boolean isEnabled(int type) { + if (type == PluginBase.PUMP) return fragmentEnabled; + else if (type == PluginBase.CONSTRAINTS) return fragmentEnabled; + return false; + } + + @Override + public boolean isVisibleInTabs(int type) { + return type == PUMP && fragmentVisible; + } + + @Override + public boolean canBeHidden(int type) { + return true; + } + + @Override + public boolean hasFragment() { + return true; + } + + @Override + public boolean showInList(int type) { + return type == PUMP; + } + + @Override + public void setFragmentEnabled(int type, boolean fragmentEnabled) { + if (type == PUMP) this.fragmentEnabled = fragmentEnabled; + } + + @Override + public void setFragmentVisible(int type, boolean fragmentVisible) { + if (type == PUMP) this.fragmentVisible = fragmentVisible; + } + + @Override + public int getPreferencesId() { + return -1; + } + + @Override + public int getType() { + return PluginBase.PUMP; + } + + @Override + public boolean isInitialized() { + return pump.initialized; + } + + @Override + public boolean isSuspended() { + return pump.state.suspended; + } + + @Override + public boolean isBusy() { + return ruffyScripter.isPumpBusy(); + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean isConnecting() { + return false; + } + + @Override + public void connect(String reason) { + // ruffyscripter establishes a connection as needed. + // ComboPlugin.runCommand performs on connect checks if needed, thus needs info on + // whether a connection is there. + // More importantly, RuffyScripter needs control over connection to be able to recover + // from a failure and deal with alarms on pump. + // Yes, this could also be done by keeping a flag 'inCmd' in RuffyScripter, which kicks + // off recovery unless set to false again after command completion and have connect + // checks be called in ComboPlugin.connect(); ... and have that cause other issues + } + + @Override + public void disconnect(String reason) { + log.debug("Disconnect called with reason: " + reason); + ruffyScripter.disconnect(); + } + + @Override + public void stopConnecting() { + // we're not doing that + } + + @Override + public synchronized PumpEnactResult setNewBasalProfile(Profile profile) { + if (!isInitialized()) { + // note that this should not happen anymore since the queue is present, which + // issues a READSTATE when starting to issue commands which initializes the pump + log.error("setNewBasalProfile not initialized"); + Notification notification = new Notification(Notification.PROFILE_NOT_SET_NOT_INITIALIZED, MainApp.gs(R.string.pumpNotInitializedProfileNotSet), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + return new PumpEnactResult().success(false).enacted(false).comment(MainApp.gs(R.string.pumpNotInitializedProfileNotSet)); + } + + BasalProfile requestedBasalProfile = convertProfileToComboProfile(profile); + if (pump.basalProfile.equals(requestedBasalProfile)) { + //dismiss previously "FAILED" overview notifications + MainApp.bus().post(new EventDismissNotification(Notification.PROFILE_NOT_SET_NOT_INITIALIZED)); + MainApp.bus().post(new EventDismissNotification(Notification.FAILED_UDPATE_PROFILE)); + return new PumpEnactResult().success(true).enacted(false); + } + + CommandResult stateResult = runCommand(null, 1, ruffyScripter::readPumpState); + if (stateResult.state.unsafeUsageDetected == PumpState.UNSUPPORTED_BASAL_RATE_PROFILE) { + return new PumpEnactResult().success(false).enacted(false).comment(MainApp.gs(R.string.combo_force_disabled_notification)); + } + + CommandResult setResult = runCommand(MainApp.gs(R.string.combo_activity_setting_basal_profile), 2, + () -> ruffyScripter.setBasalProfile(requestedBasalProfile)); + if (!setResult.success) { + Notification notification = new Notification(Notification.FAILED_UDPATE_PROFILE, MainApp.gs(R.string.failedupdatebasalprofile), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + return new PumpEnactResult().success(false).enacted(false).comment(MainApp.gs(R.string.failedupdatebasalprofile)); + } + + pump.basalProfile = requestedBasalProfile; + + //dismiss previously "FAILED" overview notifications + MainApp.bus().post(new EventDismissNotification(Notification.PROFILE_NOT_SET_NOT_INITIALIZED)); + MainApp.bus().post(new EventDismissNotification(Notification.FAILED_UDPATE_PROFILE)); + //issue success notification + Notification notification = new Notification(Notification.PROFILE_SET_OK, MainApp.gs(R.string.profile_set_ok), Notification.INFO, 60); + MainApp.bus().post(new EventNewNotification(notification)); + return new PumpEnactResult().success(true).enacted(true); + } + + @Override + public boolean isThisProfileSet(Profile profile) { + if (!isInitialized()) { + /* This might be called too soon during boot. Return true to prevent a request + to update the profile. KeepAlive is called every Constants.keepalivems + and will detect the need for a profile update and apply it. + */ + return true; + } + return pump.basalProfile.equals(convertProfileToComboProfile(profile)); + } + + @NonNull + private BasalProfile convertProfileToComboProfile(Profile profile) { + BasalProfile basalProfile = new BasalProfile(); + for (int i = 0; i < 24; i++) { + double rate = profile.getBasal(Integer.valueOf(i * 60 * 60)); + + /*The Combo pump does hava a different granularity for basal rate: + * 0.01 - if below 1U/h + * 0.05 - if above 1U/h + * */ + + if (rate < 1) { + //round to 0.01 granularity; + rate = Math.round(rate / 0.01) * 0.01; + } else { + //round to 0.05 granularity; + rate = Math.round(rate / 0.05) * 0.05; + } + + basalProfile.hourlyRates[i] = rate; + } + return basalProfile; + } + + @NonNull + @Override + public Date lastDataTime() { + return new Date(pump.lastSuccessfulCmdTime); + } + + /** Runs pump initializing if needed and reads the pump state from the main screen. */ + @Override + public synchronized void getPumpStatus() { + log.debug("getPumpStatus called"); + if (!pump.initialized) { + initializePump(); + } else { + // trigger a connect, which will update state and check history + runCommand(null, 3, ruffyScripter::readPumpState); + } + } + + private synchronized void initializePump() { + long maxWait = System.currentTimeMillis() + 15 * 1000; + while (!ruffyScripter.isPumpAvailable()) { + log.debug("Waiting for ruffy service to come up ..."); + SystemClock.sleep(100); + if (System.currentTimeMillis() > maxWait) { + log.debug("ruffy service unavailable, wtf"); + return; + } + } + + // trigger a connect, which will update state and check history + CommandResult stateResult = runCommand(null, 1, ruffyScripter::readPumpState); + if (!stateResult.success) { + return; + } + + // note that since the history is checked upon every connect, the above already updated + // the DB with any changed history records + if (pumpHistoryChanged) { + log.debug("Pump history has changed and was imported"); + pumpHistoryChanged = false; + } + + if (stateResult.state.unsafeUsageDetected == PumpState.UNSUPPORTED_BASAL_RATE_PROFILE) { + Notification n = new Notification(Notification.COMBO_PUMP_ALARM, + MainApp.gs(R.string.combo_force_disabled_notification), + Notification.URGENT); + n.soundId = R.raw.alarm; + MainApp.bus().post(new EventNewNotification(n)); + return; + } + + // read basal profile into cache (KeepAlive will trigger a profile update if needed) + CommandResult readBasalResult = runCommand(MainApp.gs(R.string.combo_actvity_reading_basal_profile), 2, ruffyScripter::readBasalProfile); + if (!readBasalResult.success) { + return; + } + pump.basalProfile = readBasalResult.basalProfile; + validBasalRateProfileSelectedOnPump = true; + + pump.initialized = true; + MainApp.bus().post(new EventInitializationChanged()); + + // show notification to check pump date if last bolus is older than 24 hours + // or is in the future + if (!recentBoluses.isEmpty()) { + long lastBolusTimestamp = recentBoluses.get(0).timestamp; + long now = System.currentTimeMillis(); + if (lastBolusTimestamp < now - 24 * 60 * 60 * 1000 || lastBolusTimestamp > now + 5 * 60 * 1000) { + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_check_date), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + } + } + + // ComboFragment updates state fully only after the pump has initialized, + // so force an update after initialization completed + MainApp.bus().post(new EventComboPumpUpdateGUI()); + } + + /** Updates local cache with state (reservoir level, last bolus ...) returned from the pump */ + private void updateLocalData(CommandResult result) { + if (result.reservoirLevel != PumpState.UNKNOWN) { + pump.reservoirLevel = result.reservoirLevel; + } + if (result.history != null && !result.history.bolusHistory.isEmpty()) { + pump.lastBolus = result.history.bolusHistory.get(0); + } + if (result.state.menu != null) { + pump.state = result.state; + } + MainApp.bus().post(new EventComboPumpUpdateGUI()); + } + + @Override + public double getBaseBasalRate() { + int currentHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + return pump.basalProfile.hourlyRates[currentHour]; + } + + private static BolusProgressReporter nullBolusProgressReporter = (state, percent, delivered) -> { + }; + + private static BolusProgressReporter bolusProgressReporter = (state, percent, delivered) -> { + EventOverviewBolusProgress event = EventOverviewBolusProgress.getInstance(); + switch (state) { + case PROGRAMMING: + event.status = MainApp.gs(R.string.combo_programming_bolus); + break; + case DELIVERING: + event.status = MainApp.gs(R.string.bolusdelivering, delivered); + break; + case DELIVERED: + event.status = MainApp.gs(R.string.bolusdelivered, delivered); + break; + case STOPPING: + event.status = MainApp.gs(R.string.bolusstopping); + break; + case STOPPED: + event.status = MainApp.gs(R.string.bolusstopped); + break; + case RECOVERING: + event.status = MainApp.gs(R.string.combo_error_bolus_recovery_progress); + } + event.percent = percent; + MainApp.bus().post(event); + }; + + /** + * Updates Treatment records with carbs and boluses and delivers a bolus if needed + */ + @Override + public PumpEnactResult deliverTreatment(DetailedBolusInfo detailedBolusInfo) { + try { + if (detailedBolusInfo.insulin == 0 && detailedBolusInfo.carbs == 0) { + // neither carbs nor bolus requested + log.error("deliverTreatment: Invalid input"); + return new PumpEnactResult().success(false).enacted(false) + .bolusDelivered(0d).carbsDelivered(0d) + .comment(MainApp.instance().getString(R.string.danar_invalidinput)); + } else if (detailedBolusInfo.insulin > 0) { + // bolus needed, ask pump to deliver it + return deliverBolus(detailedBolusInfo); + } else { + // no bolus required, carb only treatment + MainApp.getConfigBuilder().addToHistoryTreatment(detailedBolusInfo); + + EventOverviewBolusProgress bolusingEvent = EventOverviewBolusProgress.getInstance(); + bolusingEvent.t = new Treatment(); + bolusingEvent.t.isSMB = detailedBolusInfo.isSMB; + bolusingEvent.percent = 100; + MainApp.bus().post(bolusingEvent); + + return new PumpEnactResult().success(true).enacted(true) + .bolusDelivered(0d).carbsDelivered(detailedBolusInfo.carbs) + .comment(MainApp.instance().getString(R.string.virtualpump_resultok)); + } + } finally { + MainApp.bus().post(new EventComboPumpUpdateGUI()); + } + } + + @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()); + + // 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); + + // reject a bolus if one with the exact same size was successfully delivered + // within the last 1-2 minutes + if (Math.abs(previousBolus.amount - detailedBolusInfo.insulin) < 0.01 + && previousBolus.timestamp + 60 * 1000 > System.currentTimeMillis()) { + log.debug("Bolu request rejected, same bolus was successfully delivered very recently"); + return new PumpEnactResult().success(false).enacted(false) + .comment(MainApp.gs(R.string.bolus_frequency_exceeded)); + } + + // 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; + FabricPrivacy.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); + } + + BolusProgressReporter progressReporter = detailedBolusInfo.isSMB ? nullBolusProgressReporter : bolusProgressReporter; + + // start bolus delivery + scripterIsBolusing = true; + runCommand(null, 0, + () -> ruffyScripter.deliverBolus(detailedBolusInfo.insulin, progressReporter)); + 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) + .comment(MainApp.gs(R.string.combo_error_bolus_verification_failed)); + } + Bolus lastPumpBolus = postBolusStateResult.history != null && !postBolusStateResult.history.bolusHistory.isEmpty() + ? postBolusStateResult.history.bolusHistory.get(0) + : null; + + // no bolus delivered? + if (lastPumpBolus == null || lastPumpBolus.equals(previousBolus)) { + if (cancelBolus) { + return new PumpEnactResult().success(true).enacted(false); + } else { + return new PumpEnactResult() + .success(false) + .enacted(false) + .comment(MainApp.gs(R.string.combo_error_no_bolus_delivered)); + } + } + + // at least some insulin delivered, so add it to treatments + if (!addBolusToTreatments(detailedBolusInfo, lastPumpBolus)) + return new PumpEnactResult().success(false).enacted(true) + .comment(MainApp.gs(R.string.combo_error_updating_treatment_record)); + + // check pump bolus record has a sane timestamp + long now = System.currentTimeMillis(); + if (lastPumpBolus.timestamp < now - 10 * 60 * 1000 || lastPumpBolus.timestamp > now + 10 * 60 * 1000) { + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_suspious_bolus_time), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + } + + // 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) { + if (cancelBolus) { + return new PumpEnactResult().success(true).enacted(true); + } + return new PumpEnactResult().success(false).enacted(true) + .comment(MainApp.gs(R.string.combo_error_partial_bolus_delivered, + lastPumpBolus.amount, detailedBolusInfo.insulin)); + } + + // full bolus was delivered successfully + incrementBolusCount(); + return new PumpEnactResult() + .success(true) + .enacted(lastPumpBolus.amount > 0) + .bolusDelivered(lastPumpBolus.amount) + .carbsDelivered(detailedBolusInfo.carbs); + } finally { + pump.activity = null; + MainApp.bus().post(new EventComboPumpUpdateGUI()); + MainApp.bus().post(new EventRefreshOverview("Bolus")); + cancelBolus = false; + } + } + + private void incrementTbrCount() { + try { + SP.putLong(COMBO_TBRS_SET, SP.getLong(COMBO_TBRS_SET, 0L) + 1); + } catch (Exception e) { + // ignore + } + } + + private void incrementBolusCount() { + try { + SP.putLong(COMBO_BOLUSES_DELIVERED, SP.getLong(COMBO_BOLUSES_DELIVERED, 0L) + 1); + } catch (Exception e) { + // ignore + } + } + + /** + * Updates a DetailedBolusInfo from a pump bolus and adds it as a Treatment to the DB. + * Handles edge cases when dates aren't unique which are extremely unlikely to occur, + * but if they do, the user should be warned since a bolus will be missing from calculations. + */ + private boolean addBolusToTreatments(DetailedBolusInfo detailedBolusInfo, Bolus lastPumpBolus) { + DetailedBolusInfo dbi = detailedBolusInfo.copy(); + dbi.date = calculateFakeBolusDate(lastPumpBolus); + dbi.pumpId = dbi.date; + dbi.source = Source.PUMP; + dbi.insulin = lastPumpBolus.amount; + try { + boolean treatmentCreated = MainApp.getConfigBuilder().addToHistoryTreatment(dbi); + if (!treatmentCreated) { + log.error("Adding treatment record overrode an existing record: " + dbi); + if (dbi.isSMB) { + 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)); + } + FabricPrivacy.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.gs(R.string.combo_error_updating_treatment_record), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + FabricPrivacy.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; + } + return true; + } + + @Override + public void stopBolusDelivering() { + if (scripterIsBolusing) { + ruffyScripter.cancelBolus(); + } + cancelBolus = true; + } + + /** Note: AAPS calls this solely to enact OpenAPS suggestions + * + * @param force the force parameter isn't used currently since we always set the tbr - + * there might be room for optimization to first test the currently running tbr + * and only change it if it differs (as the DanaR plugin does). This approach + * might have other issues though (what happens if the tbr which wasn't re-set to + * the new value (and thus still has the old duration of e.g. 1 min) expires?) + */ + @Override + public PumpEnactResult setTempBasalAbsolute(Double absoluteRate, Integer durationInMinutes, boolean force) { + log.debug("setTempBasalAbsolute called with a rate of " + absoluteRate + " for " + durationInMinutes + " min."); + int unroundedPercentage = Double.valueOf(absoluteRate / getBaseBasalRate() * 100).intValue(); + int roundedPercentage = (int) (Math.round(absoluteRate / getBaseBasalRate() * 10) * 10); + if (unroundedPercentage != roundedPercentage) { + log.debug("Rounded requested rate " + unroundedPercentage + "% -> " + roundedPercentage + "%"); + } + + return setTempBasalPercent(roundedPercentage, durationInMinutes); + } + + /** + * Note: AAPS calls this directly only for setting a temp basal issued by the user + * + * @param forceNew Driver always applies the requested TBR and simply overrides whatever TBR + * is or isn't running at the moment + */ + @Override + public PumpEnactResult setTempBasalPercent(Integer percent, final Integer durationInMinutes, boolean forceNew) { + return setTempBasalPercent(percent, durationInMinutes); + } + + private PumpEnactResult setTempBasalPercent(Integer percent, final Integer durationInMinutes) { + log.debug("setTempBasalPercent called with " + percent + "% for " + durationInMinutes + "min"); + + if (pumpHistoryChanged && percent > 110) { + return new PumpEnactResult().success(false).enacted(false) + .comment(MainApp.gs(R.string.combo_high_temp_rejected_due_to_pump_history_changes)); + } + + int adjustedPercent = percent; + + if (adjustedPercent > pumpDescription.maxTempPercent) { + log.debug("Reducing requested TBR to the maximum support by the pump: " + percent + " -> " + pumpDescription.maxTempPercent); + adjustedPercent = pumpDescription.maxTempPercent; + } + + if (adjustedPercent % 10 != 0) { + Long rounded = Math.round(adjustedPercent / 10d) * 10; + log.debug("Rounded requested percentage:" + adjustedPercent + " -> " + rounded); + adjustedPercent = rounded.intValue(); + } + + // do a soft TBR-cancel when requested rate was rounded to 100% (>94% && <104%) + if (adjustedPercent == 100) { + return cancelTempBasal(false); + } + + int finalAdjustedPercent = adjustedPercent; + CommandResult commandResult = runCommand(MainApp.gs(R.string.combo_pump_action_setting_tbr, percent, durationInMinutes), + 3, () -> ruffyScripter.setTbr(finalAdjustedPercent, durationInMinutes)); + if (!commandResult.success) { + return new PumpEnactResult().success(false).enacted(false); + } + + PumpState state = commandResult.state; + if (state.tbrActive && state.tbrPercent == adjustedPercent + && (state.tbrRemainingDuration == durationInMinutes || state.tbrRemainingDuration == durationInMinutes - 1)) { + TemporaryBasal tempStart = new TemporaryBasal(); + tempStart.date = state.timestamp; + tempStart.durationInMinutes = state.tbrRemainingDuration; + tempStart.percentRate = state.tbrPercent; + tempStart.isAbsolute = false; + tempStart.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(tempStart); + + MainApp.bus().post(new EventComboPumpUpdateGUI()); + } + + incrementTbrCount(); + return new PumpEnactResult().success(true).enacted(true).isPercent(true) + .percent(state.tbrPercent).duration(state.tbrRemainingDuration); + } + + @Override + public PumpEnactResult setExtendedBolus(Double insulin, Integer durationInMinutes) { + return OPERATION_NOT_SUPPORTED; + } + + /** Cancel an active Temp Basal. Mostly sets a fake Temp Basal to avoid a TBR CANCELLED + * alert. This relies on TemporaryBasal objects to properly reflect the pumps state, + * which is ensured by {@link #checkAndResolveTbrMismatch(PumpState)}, which runs on each + * connect. When a hard cancel is requested, the pump is queried for it's TBR state to + * make absolutely sure no TBR is running (such a request is also made when resuming the + * loop, irregardless of whether a TBR is running or not). + */ + @Override + public PumpEnactResult cancelTempBasal(boolean enforceNew) { + log.debug("cancelTempBasal called"); + final TemporaryBasal activeTemp = MainApp.getConfigBuilder().getTempBasalFromHistory(System.currentTimeMillis()); + if (enforceNew) { + CommandResult stateResult = runCommand(MainApp.gs(R.string.combo_pump_action_refreshing), 2, ruffyScripter::readPumpState); + if (!stateResult.success) { + return new PumpEnactResult().success(false).enacted(false); + } + if (!stateResult.state.tbrActive) { + return new PumpEnactResult().success(true).enacted(false); + } + log.debug("cancelTempBasal: hard-cancelling TBR since force requested"); + CommandResult cancelResult = runCommand(MainApp.gs(R.string.combo_pump_action_cancelling_tbr), 2, ruffyScripter::cancelTbr); + if (!cancelResult.success) { + return new PumpEnactResult().success(false).enacted(false); + } + if (!cancelResult.state.tbrActive) { + TemporaryBasal tempBasal = new TemporaryBasal(); + tempBasal.date = cancelResult.state.timestamp; + tempBasal.durationInMinutes = 0; + tempBasal.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(tempBasal); + return new PumpEnactResult().isTempCancel(true).success(true).enacted(true); + } else { + return new PumpEnactResult().success(false).enacted(false); + } + } else if (activeTemp == null) { + return new PumpEnactResult().success(true).enacted(false); + } else if ((activeTemp.percentRate >= 90 && activeTemp.percentRate <= 110) && activeTemp.getPlannedRemainingMinutes() <= 15) { + // Let fake neutral temp keep run (see below) + // Note that since this runs on the queue a connection is opened regardless, but this + // case doesn't occur all that often, so it's not worth optimizing (1.3k SetTBR vs 4 cancelTBR). + log.debug("cancelTempBasal: skipping changing tbr since it already is at " + activeTemp.percentRate + "% and running for another " + activeTemp.getPlannedRemainingMinutes() + " mins."); + return new PumpEnactResult().success(true).enacted(true) + .comment("cancelTempBasal skipping changing tbr since it already is at " + + activeTemp.percentRate + "% and running for another " + + activeTemp.getPlannedRemainingMinutes() + " mins."); + } else { + // Set a fake neutral temp to avoid TBR cancel alert. Decide 90% vs 110% based on + // on whether the TBR we're cancelling is above or below 100%. + final int percentage = (activeTemp.percentRate > 100) ? 110 : 90; + log.debug("cancelTempBasal: changing TBR to " + percentage + "% for 15 mins."); + return setTempBasalPercent(percentage, 15); + } + } + + private interface CommandExecution { + CommandResult execute(); + } + + /** + * Runs a command, sets an activity if provided, retries if requested and updates fields + * concerned with last connection. + * Local cache (history, reservoir level, pump state) are updated via #updateLocalData() + * if returned by a command. + */ + private synchronized CommandResult runCommand(String activity, int retries, CommandExecution commandExecution) { + 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; + } + } + + if (activity != null) { + pump.activity = activity; + MainApp.bus().post(new EventComboPumpUpdateGUI()); + } + + commandResult = commandExecution.execute(); + + if (!commandResult.success && retries > 0) { + for (int retryAttempts = 1; !commandResult.success && retryAttempts <= retries; retryAttempts++) { + log.debug("Command was not successful, retries requested, doing retry #" + retryAttempts); + commandResult = commandExecution.execute(); + } + } + + for (Integer forwardedWarning : commandResult.forwardedWarnings) { + notifyAboutPumpWarning(new WarningOrErrorCode(forwardedWarning, null, null)); + } + + if (commandResult.success) { + pump.lastSuccessfulCmdTime = System.currentTimeMillis(); + if (validBasalRateProfileSelectedOnPump && commandResult.state.unsafeUsageDetected == PumpState.UNSUPPORTED_BASAL_RATE_PROFILE) { + validBasalRateProfileSelectedOnPump = false; + Notification n = new Notification(Notification.COMBO_PUMP_ALARM, + MainApp.gs(R.string.combo_force_disabled_notification), + Notification.URGENT); + n.soundId = R.raw.alarm; + MainApp.bus().post(new EventNewNotification(n)); + ConfigBuilderPlugin.getCommandQueue().cancelTempBasal(true, null); + } + updateLocalData(commandResult); + } + } finally { + if (activity != null) { + pump.activity = null; + MainApp.bus().post(new EventComboPumpUpdateGUI()); + } + } + + return commandResult; + } + + /** + * Returns the command result of running ReadPumpState if it wasn't successful, indicating + * an error condition. Returns null otherwise. + */ + private CommandResult runOnConnectChecks() { + // connect, get status and check if an alarm is active + CommandResult preCheckResult = ruffyScripter.readPumpState(); + if (!preCheckResult.success) { + return preCheckResult; + } + + WarningOrErrorCode activeAlert = preCheckResult.state.activeAlert; + // note if multiple alerts are active this will and should fail; e.g. if pump was stopped + // due to empty cartridge alert, which might also trigger TBR cancelled alert + if (activeAlert != null) { + if (activeAlert.warningCode != null + && (activeAlert.warningCode == PumpWarningCodes.CARTRIDGE_LOW || + activeAlert.warningCode == PumpWarningCodes.BATTERY_LOW || + activeAlert.warningCode == PumpWarningCodes.TBR_CANCELLED)) { + // turn benign warnings into notifications + notifyAboutPumpWarning(activeAlert); + ruffyScripter.confirmAlert(activeAlert.warningCode); + } else if (activeAlert.errorCode != null) { + Notification notification = new Notification(); + notification.date = new Date(); + notification.id = Notification.COMBO_PUMP_ALARM; + notification.level = Notification.URGENT; + notification.text = MainApp.gs(R.string.combo_is_in_error_state, activeAlert.errorCode, activeAlert.message); + MainApp.bus().post(new EventNewNotification(notification)); + return preCheckResult.success(false); + } + } + + checkForUnsafeUsage(preCheckResult); + checkAndResolveTbrMismatch(preCheckResult.state); + checkPumpTime(preCheckResult.state); + checkBasalRate(preCheckResult.state); + CommandResult historyCheckError = checkHistory(); + if (historyCheckError != null) { + return historyCheckError; + } + + return null; + } + + + private void checkBasalRate(PumpState state) { + if (!pump.initialized) { + // no cached profile to compare against + return; + } + if (state.unsafeUsageDetected != PumpState.SAFE_USAGE) { + // with an extended or multiwavo bolus running it's not (easily) possible + // to infer base basal rate and not supported either. Also don't compare + // if set basal rate profile is != -1. + return; + } + if (state.tbrActive && state.tbrPercent == 0) { + // can't infer base basal rate if TBR is 0 + return; + } + double pumpBasalRate = state.tbrActive + ? Math.round(state.basalRate * 100 / state.tbrPercent * 100) / 100d + : state.basalRate; + int pumpHour = new Date(state.pumpTime).getHours(); + int phoneHour = new Date().getHours(); + if (pumpHour != phoneHour) { + // only check if clocks are close + return; + } + + if (Math.abs(pumpBasalRate - getBaseBasalRate()) > 0.001) { + CommandResult readBasalResult = runCommand(MainApp.gs(R.string.combo_actvity_reading_basal_profile), 2, ruffyScripter::readBasalProfile); + if (readBasalResult.success) { + pump.basalProfile = readBasalResult.basalProfile; + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_warning_pump_basal_rate_changed), Notification.NORMAL); + MainApp.bus().post(new EventNewNotification(notification)); + } else { + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_error_failure_reading_changed_basal_rate), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + } + } + } + + /** Check pump time (on the main menu) and raise notification if time is off. + * (setting clock is not supported by ruffy) */ + private void checkPumpTime(PumpState state) { + if (state.pumpTime == 0) { + // time couldn't be read (e.g. a warning is displayed on the menu , hiding the time field) + } else if (Math.abs(state.pumpTime - System.currentTimeMillis()) >= 10 * 60 * 1000) { + log.debug("Pump clock needs update, pump time: " + state.pumpTime + " (" + new Date(state.pumpTime) + ")"); + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_notification_check_time_date), Notification.URGENT); + MainApp.bus().post(new EventNewNotification(notification)); + } else if (Math.abs(state.pumpTime - System.currentTimeMillis()) >= 3 * 60 * 1000) { + log.debug("Pump clock needs update, pump time: " + state.pumpTime + " (" + new Date(state.pumpTime) + ")"); + Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_notification_check_time_date), Notification.NORMAL); + MainApp.bus().post(new EventNewNotification(notification)); + } + } + + private void notifyAboutPumpWarning(WarningOrErrorCode activeAlert) { + if (activeAlert.warningCode == null || + (!activeAlert.warningCode.equals(PumpWarningCodes.CARTRIDGE_LOW) + && !activeAlert.warningCode.equals(PumpWarningCodes.BATTERY_LOW) + && !activeAlert.warningCode.equals(PumpWarningCodes.TBR_CANCELLED))) { + throw new IllegalArgumentException(activeAlert.toString()); + } + Notification notification = new Notification(); + notification.date = new Date(); + notification.id = Notification.COMBO_PUMP_ALARM; + notification.level = Notification.NORMAL; + if (activeAlert.warningCode == PumpWarningCodes.CARTRIDGE_LOW) { + notification.text = MainApp.gs(R.string.combo_pump_cartridge_low_warrning); + } else if (activeAlert.warningCode == PumpWarningCodes.BATTERY_LOW) { + notification.text = MainApp.gs(R.string.combo_pump_battery_low_warrning); + } else if (activeAlert.warningCode == PumpWarningCodes.TBR_CANCELLED) { + notification.text = MainApp.gs(R.string.combo_pump_tbr_cancelled_warrning); + } + MainApp.bus().post(new EventNewNotification(notification)); + } + + private void checkForUnsafeUsage(CommandResult commandResult) { + if (commandResult == null) return; + + long lastViolation = 0; + if (commandResult.state.unsafeUsageDetected == PumpState.UNSUPPORTED_BOLUS_TYPE) { + lastViolation = System.currentTimeMillis(); + } else if (commandResult.history != null) { + for (Bolus bolus : commandResult.history.bolusHistory) { + if (!bolus.isValid && bolus.timestamp > lastViolation) { + lastViolation = bolus.timestamp; + } + } + } + if (lastViolation > 0) { + lowSuspendOnlyLoopEnforcedUntil = lastViolation + 6 * 60 * 60 * 1000; + if (lowSuspendOnlyLoopEnforcedUntil > System.currentTimeMillis() && violationWarningRaisedForBolusAt != lowSuspendOnlyLoopEnforcedUntil) { + Notification n = new Notification(Notification.COMBO_PUMP_ALARM, + MainApp.gs(R.string.combo_low_suspend_forced_notification), + Notification.URGENT); + n.soundId = R.raw.alarm; + MainApp.bus().post(new EventNewNotification(n)); + violationWarningRaisedForBolusAt = lowSuspendOnlyLoopEnforcedUntil; + ConfigBuilderPlugin.getCommandQueue().cancelTempBasal(true, null); + } + } + } + + /** + * Checks the main screen to determine if TBR on pump matches app state. + */ + private void checkAndResolveTbrMismatch(PumpState state) { + // compare with: info.nightscout.androidaps.plugins.PumpDanaR.comm.MsgStatusTempBasal.updateTempBasalInDB() + long now = System.currentTimeMillis(); + TemporaryBasal aapsTbr = MainApp.getConfigBuilder().getTempBasalFromHistory(now); + if (aapsTbr == null && state.tbrActive && state.tbrRemainingDuration > 2) { + log.debug("Creating temp basal from pump TBR"); + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboTbrMismatch") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("type", "new TBR on pump")); + TemporaryBasal newTempBasal = new TemporaryBasal(); + newTempBasal.date = now; + newTempBasal.percentRate = state.tbrPercent; + newTempBasal.isAbsolute = false; + newTempBasal.durationInMinutes = state.tbrRemainingDuration; + newTempBasal.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(newTempBasal); + } else if (aapsTbr != null && aapsTbr.getPlannedRemainingMinutes() > 2 && !state.tbrActive) { + log.debug("Ending AAPS-TBR since pump has no TBR active"); + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboTbrMismatch") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("type", "TBR cancelled on pump")); + TemporaryBasal tempStop = new TemporaryBasal(); + tempStop.date = now; + tempStop.durationInMinutes = 0; + tempStop.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(tempStop); + } else if (aapsTbr != null && state.tbrActive + && (aapsTbr.percentRate != state.tbrPercent || + Math.abs(aapsTbr.getPlannedRemainingMinutes() - state.tbrRemainingDuration) > 2)) { + log.debug("AAPSs and pump-TBR differ; ending AAPS-TBR and creating new TBR based on pump TBR"); + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboTbrMismatch") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("type", "TBR on pump differs from AAPS TBR")); + TemporaryBasal tempStop = new TemporaryBasal(); + tempStop.date = now - 1000; + tempStop.durationInMinutes = 0; + tempStop.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(tempStop); + + TemporaryBasal newTempBasal = new TemporaryBasal(); + newTempBasal.date = now; + newTempBasal.percentRate = state.tbrPercent; + newTempBasal.isAbsolute = false; + newTempBasal.durationInMinutes = state.tbrRemainingDuration; + newTempBasal.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(newTempBasal); + } + } + + /**Reads the pump's history and updates the DB accordingly. */ + private boolean readHistory(@Nullable PumpHistoryRequest request) { + CommandResult historyResult = runCommand(MainApp.gs(R.string.combo_activity_reading_pump_history), 3, () -> ruffyScripter.readHistory(request)); + if (!historyResult.success) { + return false; + } + + PumpHistory history = historyResult.history; + updateDbFromPumpHistory(history); + + // update local cache + if (!history.pumpAlertHistory.isEmpty()) { + pump.errorHistory = history.pumpAlertHistory; + } + if (!history.tddHistory.isEmpty()) { + pump.tddHistory = history.tddHistory; + } + + return historyResult.success; + } + + private boolean updateDbFromPumpHistory(@NonNull PumpHistory history) { + boolean updated = false; + for (Bolus pumpBolus : history.bolusHistory) { + DetailedBolusInfo dbi = new DetailedBolusInfo(); + dbi.date = calculateFakeBolusDate(pumpBolus); + dbi.pumpId = dbi.date; + dbi.source = Source.PUMP; + dbi.insulin = pumpBolus.amount; + dbi.eventType = CareportalEvent.CORRECTIONBOLUS; + if (MainApp.getConfigBuilder().addToHistoryTreatment(dbi)) { + updated = true; + } + } + return updated; + } + + /** 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 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. + */ + 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 + void readTddData(Callback post) { +// ConfigBuilderPlugin.getCommandQueue().custom(new Callback() { +// @Override +// public void run() { + readHistory(new PumpHistoryRequest().tddHistory(PumpHistoryRequest.FULL)); +// } +// }, post); + if (post != null) { + post.run(); + } + CommandQueue commandQueue = ConfigBuilderPlugin.getCommandQueue(); + if (commandQueue.performing() == null && commandQueue.size() == 0) { + ruffyScripter.disconnect(); + } + } + + // TODO use queue once ready + void readAlertData(Callback post) { +// ConfigBuilderPlugin.getCommandQueue().custom(new Callback() { +// @Override +// public void run() { + readHistory(new PumpHistoryRequest().pumpErrorHistory(PumpHistoryRequest.FULL)); +// } +// }, post); + if (post != null) { + post.run(); + } + CommandQueue commandQueue = ConfigBuilderPlugin.getCommandQueue(); + if (commandQueue.performing() == null && commandQueue.size() == 0) { + ruffyScripter.disconnect(); + } + } + + // TODO use queue once ready + void readAllPumpData(Callback post) { +// ConfigBuilderPlugin.getCommandQueue().custom(new Callback() { +// @Override +// public void run() { + readHistory(new PumpHistoryRequest() + .bolusHistory(PumpHistoryRequest.FULL) + .pumpErrorHistory(PumpHistoryRequest.FULL) + .tddHistory(PumpHistoryRequest.FULL)); + CommandResult readBasalResult = runCommand(MainApp.gs(R.string.combo_actvity_reading_basal_profile), 2, ruffyScripter::readBasalProfile); + if (readBasalResult.success) { + pump.basalProfile = readBasalResult.basalProfile; + } +// } +// }, post); + if (post != null) { + post.run(); + } + CommandQueue commandQueue = ConfigBuilderPlugin.getCommandQueue(); + if (commandQueue.performing() == null && commandQueue.size() == 0) { + ruffyScripter.disconnect(); + } + } + + /** + * Reads QuickInfo to update reservoir level and determine if new boluses exist on the pump + * and if so, queries the history for all new records. + * + * @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)); + + // 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; + } + + // compare recent records + 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; + } + + // 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(lastKnownPumpRecordTimestamp))); + if (!historyResult.success) { + pumpHistoryChanged = true; + 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."); + FabricPrivacy.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)); + } + + pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history); + if (pumpHistoryChanged) { + log.debug("Setting 'pumpHistoryChanged' true"); + } + + List updatedPumpBolusHistory = historyResult.history.bolusHistory; + if (!updatedPumpBolusHistory.isEmpty()) { + recentBoluses = updatedPumpBolusHistory.subList(0, Math.min(updatedPumpBolusHistory.size(), 2)); + } + + return null; + } + + @Override + public PumpEnactResult cancelExtendedBolus() { + return OPERATION_NOT_SUPPORTED; + } + + @Override + public JSONObject getJSONStatus(Profile profile, String profileName) { + if (!pump.initialized) { + return null; + } + + try { + JSONObject pumpJson = new JSONObject(); + pumpJson.put("clock", DateUtil.toISOString(pump.lastSuccessfulCmdTime)); + + int level; + if (pump.reservoirLevel != -1) level = pump.reservoirLevel; + else if (pump.state.insulinState == PumpState.LOW) level = 8; + else if (pump.state.insulinState == PumpState.EMPTY) level = 0; + else level = 150; + pumpJson.put("reservoir", level); + + JSONObject statusJson = new JSONObject(); + statusJson.put("status", getStateSummary()); + statusJson.put("timestamp", pump.lastSuccessfulCmdTime); + pumpJson.put("status", statusJson); + + JSONObject extendedJson = new JSONObject(); + extendedJson.put("Version", BuildConfig.VERSION_NAME + "-" + BuildConfig.BUILDVERSION); + extendedJson.put("ActiveProfile", MainApp.getConfigBuilder().getProfileName()); + PumpState ps = pump.state; + if (ps.tbrActive) { + extendedJson.put("TempBasalAbsoluteRate", ps.basalRate); + extendedJson.put("TempBasalPercent", ps.tbrPercent); + extendedJson.put("TempBasalRemaining", ps.tbrRemainingDuration); + } + if (ps.activeAlert != null && ps.activeAlert.errorCode != null) { + extendedJson.put("ErrorCode", ps.activeAlert.errorCode); + } + pumpJson.put("extended", extendedJson); + + JSONObject batteryJson = new JSONObject(); + int battery = 100; + if (ps.batteryState == PumpState.LOW) battery = 25; + else if (ps.batteryState == PumpState.EMPTY) battery = 0; + batteryJson.put("percent", battery); + pumpJson.put("battery", batteryJson); + + return pumpJson; + } catch (Exception e) { + log.warn("Failed to gather device status for upload", e); + } + + return null; + } + + @Override + public String deviceID() { + return "Combo"; + } + + @Override + public PumpDescription getPumpDescription() { + return pumpDescription; + } + + @Override + public String shortStatus(boolean veryShort) { + return getStateSummary(); + } + + @Override + public boolean isFakingTempsByExtendedBoluses() { + return false; + } + + // Constraints interface + private long lowSuspendOnlyLoopEnforcedUntil = 0; + private long violationWarningRaisedForBolusAt = 0; + private boolean validBasalRateProfileSelectedOnPump = true; + + @Override + public boolean isLoopEnabled() { + return validBasalRateProfileSelectedOnPump; + } + + @Override + public boolean isClosedModeEnabled() { + return true; + } + + @Override + public boolean isAutosensModeEnabled() { + return true; + } + + @Override + public boolean isAMAModeEnabled() { + return true; + } + + @Override + public boolean isSMBModeEnabled() { + return true; + } + + @Override + public Double applyBasalConstraints(Double absoluteRate) { + return absoluteRate; + } + + @Override + public Integer applyBasalConstraints(Integer percentRate) { + return percentRate; + } + + @Override + public Double applyBolusConstraints(Double insulin) { + return insulin; + } + + @Override + public Integer applyCarbsConstraints(Integer carbs) { + return carbs; + } + + @Override + public Double applyMaxIOBConstraints(Double maxIob) { + return lowSuspendOnlyLoopEnforcedUntil < System.currentTimeMillis() ? maxIob : 0; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPump.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPump.java new file mode 100644 index 0000000000..aef0f2abd5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboPump.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.PumpCombo; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BasalProfile; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpState; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpAlert; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Tdd; + +class ComboPump { + boolean initialized = false; + volatile long lastSuccessfulCmdTime; + + public volatile String activity; + @NonNull + volatile PumpState state = new PumpState(); + volatile int reservoirLevel = -1; + @NonNull + volatile BasalProfile basalProfile = new BasalProfile(); + @Nullable + volatile Bolus lastBolus; + + // Alert and TDD histories are not stored in DB, but are read on demand and just cached here + List errorHistory = new ArrayList<>(0); + List tddHistory = new ArrayList<>(0); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboTddHistoryDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboTddHistoryDialog.java new file mode 100644 index 0000000000..7da1c8ba30 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ComboTddHistoryDialog.java @@ -0,0 +1,58 @@ +package info.nightscout.androidaps.plugins.PumpCombo; + +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.text.DateFormat; +import java.util.List; +import java.util.Locale; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Tdd; +import info.nightscout.androidaps.R; + +public class ComboTddHistoryDialog extends DialogFragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.combo_tdd_history_fragment, container, false); + TextView text = (TextView) layout.findViewById(R.id.combo_tdd_history_text); + List tdds = ComboPlugin.getPlugin().getPump().tddHistory; + if (tdds.isEmpty()) { + text.setText(R.string.combo_no_tdd_data_note); + } else { + StringBuilder sb = new StringBuilder(); + DateFormat dateFormatter = DateFormat.getDateInstance(); + double avg = 0; + double min = 999; + double max = 0; + int count = 0; + for (Tdd tdd : tdds) { + if (tdd.total > 0) { + avg += tdd.total; + count++; + } + if (tdd.total < min) min = tdd.total; + if (tdd.total > max) max = tdd.total; + } + avg = avg / count; + sb.append(String.format(Locale.getDefault(), getString(R.string.combo_tdd_minimum), min)); + sb.append("\n"); + sb.append(String.format(Locale.getDefault(), getString(R.string.combo_tdd_average), avg)); + sb.append("\n"); + sb.append(String.format(Locale.getDefault(), getString(R.string.combo_tdd_maximum), max)); + sb.append("\n"); + for (Tdd tdd : tdds) { + sb.append("\n"); + sb.append(dateFormatter.format(tdd.timestamp)); + sb.append(" "); + sb.append(String.format(Locale.getDefault(), "%3.1f", tdd.total)); + sb.append(" U"); + } + text.setText(sb.toString()); + } + return layout; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/events/EventComboPumpUpdateGUI.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/events/EventComboPumpUpdateGUI.java new file mode 100644 index 0000000000..e9bf3f8415 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/events/EventComboPumpUpdateGUI.java @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.PumpCombo.events; + +/** + * Created by mike on 24.05.2017. + */ + +public class EventComboPumpUpdateGUI { +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/BasalProfile.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/BasalProfile.java new file mode 100644 index 0000000000..fa9c2dceb0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/BasalProfile.java @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +import java.util.Arrays; + +public class BasalProfile { + public final double[] hourlyRates; + + public BasalProfile() { + this.hourlyRates = new double[24]; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BasalProfile that = (BasalProfile) o; + + for(int i = 0; i < 24; i++) { + if (Math.abs(hourlyRates[i] - that.hourlyRates[i]) > 0.001) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(hourlyRates); + } + + @Override + public String toString() { + double total = 0d; + for(int i = 0; i < 24; i++) { + total += hourlyRates[i]; + } + return "BasalProfile{" + + "hourlyRates=" + Arrays.toString(hourlyRates) + ", total " + total + " U" + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/BolusProgressReporter.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/BolusProgressReporter.java new file mode 100644 index 0000000000..59d334fe6f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/BolusProgressReporter.java @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +public interface BolusProgressReporter { + enum State { + PROGRAMMING, + DELIVERING, + DELIVERED, + STOPPING, + STOPPED, + RECOVERING + } + + void report(State state, int percent, double delivered); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/CommandResult.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/CommandResult.java new file mode 100644 index 0000000000..9da5d4d6fa --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/CommandResult.java @@ -0,0 +1,58 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +import android.support.annotation.Nullable; + +import java.util.LinkedList; +import java.util.List; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistory; + +public class CommandResult { + /** Whether the command was executed successfully. */ + public boolean success; + /** State of the pump *after* command execution. */ + public PumpState state; + /** History if requested by the command. */ + @Nullable + public PumpHistory history; + /** Basal rate profile if requested. */ + public BasalProfile basalProfile; + + /** Warnings raised on the pump that are forwarded to AAPS to be turned into AAPS + * notifications. */ + public List forwardedWarnings = new LinkedList<>(); + + public int reservoirLevel = -1; + + public CommandResult success(boolean success) { + this.success = success; + return this; + } + + public CommandResult state(PumpState state) { + this.state = state; + return this; + } + + public CommandResult history(PumpHistory history) { + this.history = history; + return this; + } + + public CommandResult basalProfile(BasalProfile basalProfile) { + this.basalProfile = basalProfile; + return this; + } + + @Override + public String toString() { + return "CommandResult{" + + "success=" + success + + ", state=" + state + + ", history=" + history + + ", basalProfile=" + basalProfile + + ", forwardedWarnings='" + forwardedWarnings + '\'' + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpErrorCodes.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpErrorCodes.java new file mode 100644 index 0000000000..63cd81e3a6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpErrorCodes.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +public class PumpErrorCodes { + public static final int CARTRIDGE_EMPTY = 1; + public static final int BATTERY_EMPTY = 2; + public static final int AUTOMATIC_OFF = 3; + public static final int OCCLUSION = 4; + public static final int END_OF_OPERATION_BACKUP_PUMP = 5; + public static final int MECHANICAL_ERROR = 6; + public static final int ELECTRONIC_ERROR = 7; + public static final int POWER_INTERRUPT = 8; + public static final int END_OF_OPERATION_LOAN_PUMP = 9; + public static final int CARTRIDGE_ERROR = 10; + public static final int SET_NOT_PRIMED = 11; + public static final int DATA_INTERRUPTED = 12; + public static final int LANGUAGE_ERROR = 13; +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpState.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpState.java new file mode 100644 index 0000000000..963cdd8b4c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpState.java @@ -0,0 +1,103 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +/** State displayed on the main screen of the pump. */ +public class PumpState { + /** Time the state was captured. */ + public long timestamp; + /** Pump time. Note that this is derived from the time displayed on the main menu and assumes + * the date is set correctly */ + public long pumpTime; + public String menu = null; + public boolean suspended; + + public boolean tbrActive = false; + /** TBR percentage. 100% means no TBR active, just the normal basal rate running. */ + public int tbrPercent = -1; + /** The absolute rate the pump is running (regular basal rate or TBR), e.g. 0.80U/h. */ + public double basalRate = -1; + /** Remaining time of an active TBR. Note that 0:01 is te lowest displayed, the pump + * jumps from that to TBR end, skipping 0:00(xx). */ + public int tbrRemainingDuration = -1; + + /** Warning or error code displayed if a warning or alert alert is active, + * see {@link PumpWarningCodes}, {@link PumpErrorCodes} */ + public WarningOrErrorCode activeAlert; + + public static final int UNKNOWN = -1; + public static final int LOW = 1; + public static final int EMPTY = 2; + public int batteryState = UNKNOWN; + public int insulinState = UNKNOWN; + + public int activeBasalProfileNumber; + + public static final int SAFE_USAGE = 0; + public static final int UNSUPPORTED_BOLUS_TYPE = 1; + public static final int UNSUPPORTED_BASAL_RATE_PROFILE = 2; + /** True if use of an extended or multiwave bolus has been detected */ + public int unsafeUsageDetected = SAFE_USAGE; + + public PumpState menu(String menu) { + this.menu = menu; + return this; + } + + public PumpState tbrActive(boolean tbrActive) { + this.tbrActive = tbrActive; + return this; + } + + public PumpState tbrPercent(int tbrPercent) { + this.tbrPercent = tbrPercent; + return this; + } + + public PumpState basalRate(double basalRate) { + this.basalRate = basalRate; + return this; + } + + public PumpState tbrRemainingDuration(int tbrRemainingDuration) { + this.tbrRemainingDuration = tbrRemainingDuration; + return this; + } + + public PumpState suspended(boolean suspended) { + this.suspended = suspended; + return this; + } + + public PumpState batteryState(int batteryState) { + this.batteryState = batteryState; + return this; + } + + public PumpState insulinState(int insulinState) { + this.insulinState = insulinState; + return this; + } + + public PumpState activeBasalProfileNumber(int activeBasalProfileNumber) { + this.activeBasalProfileNumber = activeBasalProfileNumber; + return this; + } + + @Override + public String toString() { + return "PumpState{" + + "timestamp=" + timestamp + + ", pumpTime=" + pumpTime + + ", menu='" + menu + '\'' + + ", suspended=" + suspended + + ", tbrActive=" + tbrActive + + ", tbrPercent=" + tbrPercent + + ", basalRate=" + basalRate + + ", tbrRemainingDuration=" + tbrRemainingDuration + + ", activeAlert=" + activeAlert + + ", batteryState=" + batteryState + + ", insulinState=" + insulinState + + ", activeBasalProfileNumber=" + activeBasalProfileNumber + + ", unsafeUsageDetected=" + unsafeUsageDetected + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpWarningCodes.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpWarningCodes.java new file mode 100644 index 0000000000..60d1898834 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/PumpWarningCodes.java @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +public class PumpWarningCodes { + public static final int CARTRIDGE_LOW = 1; + public static final int BATTERY_LOW = 2; + public static final int REVIEW_TIME = 3; + public static final int CALL_FOR_UPDATE = 4; + public static final int PUMP_TIMER = 5; + public static final int TBR_CANCELLED = 6; + public static final int TBR_OVER = 7; + public static final int BOLUS_CANCELLED = 8; + public static final int LOANTIME_WARNING = 9; + public static final int BLUETOOTH_FAULT = 10; +} 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 new file mode 100644 index 0000000000..5918023d1e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyCommands.java @@ -0,0 +1,49 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistoryRequest; + +public interface RuffyCommands { + /** Issues a bolus issues updates on progress through via {@link BolusProgressReporter}. */ + CommandResult deliverBolus(double amount, BolusProgressReporter bolusProgressReporter); + + /** Requests cancellation of an active bolus if possible. */ + void cancelBolus(); + + CommandResult setTbr(int percent, int duration); + + CommandResult cancelTbr(); + + /** Confirms an active warning alert on the pump. + * @see PumpWarningCodes */ + CommandResult confirmAlert(int warningCode); + + /** Indicate if the pump is ready to receive commands. */ + boolean isPumpAvailable(); + + /** Indicate of the pump is busy processing a command. */ + boolean isPumpBusy(); + + /** Whether there's usable connection to the pump. */ + boolean isConnected(); + + void disconnect(); + + /** Read the state of the pump, which encompasses all information displayed on the main menu. */ + CommandResult readPumpState(); + + /** Read reservoir level and last bolus via Quick Info */ + 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. */ + CommandResult readHistory(PumpHistoryRequest request); + + CommandResult readBasalProfile(); + + CommandResult setBasalProfile(BasalProfile basalProfile); + + CommandResult getDateAndTime(); + + CommandResult setDateAndTime(); +} + 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 new file mode 100644 index 0000000000..8b923d51ff --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/RuffyScripter.java @@ -0,0 +1,943 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.crashlytics.android.answers.Answers; +import com.crashlytics.android.answers.CustomEvent; +import com.google.common.base.Joiner; + +import org.monkey.d.ruffy.ruffy.driver.IRTHandler; +import org.monkey.d.ruffy.ruffy.driver.IRuffyService; +import org.monkey.d.ruffy.ruffy.driver.display.Menu; +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.MenuTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.ReadQuickInfoCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistoryRequest; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.BolusCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.CancelTbrCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.Command; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.CommandException; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.ConfirmAlertCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.ReadBasalProfileCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.ReadHistoryCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.ReadPumpStateCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.SetBasalProfileCommand; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands.SetTbrCommand; +import info.nightscout.androidaps.BuildConfig; +import info.nightscout.utils.FabricPrivacy; + +/** + * Provides scripting 'runtime' and operations. consider moving operations into a separate + * class and inject that into executing commands, so that commands operately solely on + * operations and are cleanly separated from the thread management, connection management etc + */ +public class RuffyScripter implements RuffyCommands { + private static final Logger log = LoggerFactory.getLogger(RuffyScripter.class); + + private IRuffyService ruffyService; + + @Nullable + private volatile Menu currentMenu; + private volatile long menuLastUpdated = 0; + private volatile boolean unparsableMenuEncountered; + + + private String previousCommand = ""; + private volatile Command activeCmd = null; + + private boolean started = false; + + private final Object screenlock = new Object(); + + private IRTHandler mHandler = new IRTHandler.Stub() { + @Override + public void log(String message) throws RemoteException { + if (log.isTraceEnabled()) { + log.trace("Ruffy says: " + message); + } + } + + @Override + public void fail(String message) throws RemoteException { + log.warn("Ruffy warns: " + message); + if (message.startsWith("no connection possible")) + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRuffyWarning").putCustomAttribute("message", "no connection possible")); + else if (message.startsWith("Error sending keep alive while rtModeRunning is still true")) + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRuffyWarning").putCustomAttribute("message", "Error sending keep alive while rtModeRunning is still true")); + else if (message.startsWith("Error sending keep alive. rtModeRunning is false, so this is most likely a race condition during disconnect")) + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRuffyWarning").putCustomAttribute("message", "Error sending keep alive. rtModeRunning is false, so this is most likely a race condition during disconnect")); + else + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRuffyWarning").putCustomAttribute("message", message.substring(0, 98))); + } + + @Override + public void requestBluetooth() throws RemoteException { + log.trace("Ruffy invoked requestBluetooth callback"); + } + + @Override + public void rtStopped() throws RemoteException { + log.debug("rtStopped callback invoked"); + currentMenu = null; + } + + @Override + public void rtStarted() throws RemoteException { + log.debug("rtStarted callback invoked"); + } + + @Override + public void rtClearDisplay() throws RemoteException { + } + + @Override + public void rtUpdateDisplay(byte[] quarter, int which) throws RemoteException { + } + + @Override + public void rtDisplayHandleMenu(Menu menu) throws RemoteException { + // method is called every ~500ms + log.debug("rtDisplayHandleMenu: " + menu); + + currentMenu = menu; + menuLastUpdated = System.currentTimeMillis(); + + synchronized (screenlock) { + screenlock.notifyAll(); + } + } + + @Override + public void rtDisplayHandleNoMenu() throws RemoteException { + log.warn("rtDisplayHandleNoMenu callback invoked"); + unparsableMenuEncountered = true; + } + }; + + public RuffyScripter(Context context) { + boolean boundSucceeded = false; + + try { + Intent intent = new Intent() + .setComponent(new ComponentName( + // this must be the base package of the app (check package attribute in + // manifest element in the manifest file of the providing app) + "org.monkey.d.ruffy.ruffy", + // full path to the driver; + // in the logs this service is mentioned as (note the slash) + // "org.monkey.d.ruffy.ruffy/.driver.Ruffy"; + // org.monkey.d.ruffy.ruffy is the base package identifier + // and /.driver.Ruffy the service within the package + "org.monkey.d.ruffy.ruffy.driver.Ruffy" + )); + context.startService(intent); + + ServiceConnection mRuffyServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + log.debug("ruffy service connected"); + ruffyService = IRuffyService.Stub.asInterface(service); + try { + ruffyService.setHandler(mHandler); + } catch (Exception e) { + log.error("Ruffy handler has issues", e); + } + started = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + log.debug("ruffy service disconnected"); + } + }; + boundSucceeded = context.bindService(intent, mRuffyServiceConnection, Context.BIND_AUTO_CREATE); + } catch (Exception e) { + log.error("Binding to ruffy service failed", e); + } + + if (!boundSucceeded) { + log.error("No connection to ruffy. Pump control unavailable."); + } else { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboScripterInit") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + } + } + + @Override + public boolean isPumpAvailable() { + return started; + } + + @Override + public boolean isPumpBusy() { + return activeCmd != null; + } + + @Override + public boolean isConnected() { + if (ruffyService == null) { + return false; + } + try { + if (!ruffyService.isConnected()) { + return false; + } + return ruffyService.isConnected() && System.currentTimeMillis() - menuLastUpdated < 10 * 1000; + } catch (RemoteException e) { + return false; + } + } + + @Override + public synchronized void disconnect() { + if (ruffyService == null) { + return; + } + try { + log.debug("Disconnecting"); + ruffyService.doRTDisconnect(); + } catch (RemoteException e) { + // ignore + } catch (Exception e) { + log.warn("Disconnect not happy", e); + } + } + + @Override + public CommandResult readPumpState() { + return runCommand(new ReadPumpStateCommand()); + } + + @Override + public CommandResult readQuickInfo(int numberOfBolusRecordsToRetrieve) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboReadQuickInfoCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new ReadQuickInfoCommand(numberOfBolusRecordsToRetrieve)); + } + + public void returnToRootMenu() { + // returning to main menu using the 'back' key does not cause a vibration + MenuType menuType = getCurrentMenu().getType(); + while (menuType != MenuType.MAIN_MENU && menuType != MenuType.STOP && menuType != MenuType.WARNING_OR_ERROR) { + log.debug("Going back to main menu, currently at " + menuType); + pressBackKey(); + while (getCurrentMenu().getType() == menuType) { + waitForScreenUpdate(); + } + menuType = getCurrentMenu().getType(); + } + } + + /** + * Always returns a CommandResult, never throws + */ + private CommandResult runCommand(final Command cmd) { + log.debug("Attempting to run cmd: " + cmd); + + List violations = cmd.validateArguments(); + if (!violations.isEmpty()) { + log.error("Command argument violations: " + Joiner.on(", ").join(violations)); + return new CommandResult().success(false).state(new PumpState()); + } + + synchronized (RuffyScripter.class) { + Thread cmdThread = null; + try { + activeCmd = cmd; + long connectStart = System.currentTimeMillis(); + ensureConnected(); + log.debug("Connection ready to execute cmd " + cmd); + cmdThread = new Thread(() -> { + try { + if (!runPreCommandChecks(cmd)) { + return; + } + PumpState pumpState = readPumpStateInternal(); + log.debug("Pump state before running command: " + pumpState); + + // execute the command + cmd.setScripter(RuffyScripter.this); + long cmdStartTime = System.currentTimeMillis(); + cmd.execute(); + long cmdEndTime = System.currentTimeMillis(); + log.debug("Executing " + cmd + " took " + (cmdEndTime - cmdStartTime) + "ms"); + } catch (CommandException e) { + log.error("CommandException running command", e); + activeCmd.getResult().success = false; + } catch (Exception e) { + log.error("Unexpected exception running cmd", e); + activeCmd.getResult().success = false; + } + }, cmd.getClass().getSimpleName()); + long executionStart = System.currentTimeMillis(); + cmdThread.start(); + + long overallTimeout = System.currentTimeMillis() + 10 * 60 * 1000; + while (cmdThread.isAlive()) { + if (!isConnected()) { + // on connection loss try to reconnect, confirm warning alerts caused by + // the disconnected and then return the command as failed (the caller + // can retry if needed). + log.debug("Connection unusable (ruffy connection: " + ruffyService.isConnected() + ", " + + "time since last menu update: " + (System.currentTimeMillis() - menuLastUpdated) + " ms, " + + "aborting command and attempting reconnect ..."); + cmdThread.interrupt(); + activeCmd.getResult().success = false; + + // the BT connection might be still there, but we might not be receiving + // menu updates, so force a disconnect before connecting again + disconnect(); + SystemClock.sleep(500); + for (int attempts = 2; attempts > 0; attempts--) { + boolean reconnected = recoverFromConnectionLoss(); + if (reconnected) { + break; + } + // connect attempt times out after 90s, shortly wait and then retry; + // (90s timeout + 5s wait) * 2 attempts = 190s + SystemClock.sleep(5 * 1000); + } + break; + } + + if (System.currentTimeMillis() > overallTimeout) { + log.error("Command " + cmd + " timed out"); + cmdThread.interrupt(); + activeCmd.getResult().success = false; + break; + } + + if (unparsableMenuEncountered) { + log.error("UnparsableMenuEncountered flagged, aborting command"); + cmdThread.interrupt(); + activeCmd.getResult().success = false; + } + + log.trace("Waiting for running command to complete"); + SystemClock.sleep(500); + } + + activeCmd.getResult().state = readPumpStateInternal(); + CommandResult result = activeCmd.getResult(); + if (log.isDebugEnabled()) { + long connectDurationSec = (executionStart - connectStart) / 1000; + long executionDurationSec = (System.currentTimeMillis() - executionStart) / 1000; + log.debug("Command result: " + result); + log.debug("Connect: " + connectDurationSec + "s, execution: " + executionDurationSec + "s"); + } + return result; + } catch (CommandException e) { + log.error("CommandException while executing command", e); + PumpState pumpState = recoverFromCommandFailure(); + return activeCmd.getResult().success(false).state(pumpState); + } catch (Exception e) { + log.error("Unexpected exception communication with ruffy", e); + PumpState pumpState = recoverFromCommandFailure(); + return activeCmd.getResult().success(false).state(pumpState); + } finally { + Menu menu = this.currentMenu; + if (activeCmd.getResult().success && menu != null && menu.getType() != MenuType.MAIN_MENU) { + log.warn("Command " + activeCmd + " successful, but finished leaving pump on menu " + getCurrentMenuName()); + } + if (cmdThread != null) { + try { + // let command thread finish updating activeCmd var + cmdThread.join(1000); + } catch (InterruptedException e) { + // ignore + } + } + previousCommand = "" + activeCmd; + activeCmd = null; + } + } + } + + private boolean runPreCommandChecks(Command cmd) { + if (cmd instanceof ReadPumpStateCommand) { + // always allowed, state is set at the end of runCommand method + activeCmd.getResult().success = true; + } else if (getCurrentMenu().getType() == MenuType.STOP) { + if (cmd.needsRunMode()) { + log.error("Requested command requires run mode, but pump is suspended"); + activeCmd.getResult().success = false; + return false; + } + } else if (getCurrentMenu().getType() == MenuType.WARNING_OR_ERROR) { + if (!(cmd instanceof ConfirmAlertCommand)) { + log.warn("Warning/alert active on pump, but requested command is not ConfirmAlertCommand"); + activeCmd.getResult().success = false; + return false; + } + } else if (getCurrentMenu().getType() != MenuType.MAIN_MENU) { + log.debug("Pump is unexpectedly not on main menu but " + getCurrentMenuName() + ", trying to recover"); + try { + recoverFromCommandFailure(); + } catch (Exception e) { + activeCmd.getResult().success = false; + return false; + } + if (getCurrentMenu().getType() != MenuType.MAIN_MENU) { + activeCmd.getResult().success = false; + return false; + } + } + return true; + } + + /** + * On connection loss the pump raises an alert immediately (when setting a TBR or giving a bolus) - + * there's no timeout before that happens. But: a reconnect is still possible which can then + * confirm the alert. + * + * @return whether the reconnect and return to main menu was successful + */ + private boolean recoverFromConnectionLoss() { + log.debug("Connection was lost, trying to reconnect"); + ensureConnected(); + if (getCurrentMenu().getType() == MenuType.WARNING_OR_ERROR) { + WarningOrErrorCode warningOrErrorCode = readWarningOrErrorCode(); + if (Objects.equals(activeCmd.getReconnectWarningId(), warningOrErrorCode.warningCode)) { + log.debug("Confirming warning caused by disconnect: #" + warningOrErrorCode.warningCode); + // confirm alert + verifyMenuIsDisplayed(MenuType.WARNING_OR_ERROR); + pressCheckKey(); + // dismiss alert + verifyMenuIsDisplayed(MenuType.WARNING_OR_ERROR); + pressCheckKey(); + } + } + + boolean connected = isConnected(); + if (connected) { + MenuType menuType = getCurrentMenu().getType(); + if (menuType != MenuType.MAIN_MENU && menuType != MenuType.WARNING_OR_ERROR) { + returnToRootMenu(); + } + } + log.debug("Recovery from connection loss " + (connected ? "succeeded" : "failed")); + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRecoveryFromConnectionLoss") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) + .putCustomAttribute("success", connected ? "true" : "else")); + return connected; + } + + /** + * Returns to the main menu (if possible) after a command failure, so that subsequent commands + * reusing the connection won't fail and returns the current PumpState (empty if unreadable). + */ + private PumpState recoverFromCommandFailure() { + Menu menu = this.currentMenu; + if (menu == null) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) + .putCustomAttribute("exit", "1") + .putCustomAttribute("success", "false")); + return new PumpState(); + } + MenuType type = menu.getType(); + if (type != MenuType.WARNING_OR_ERROR && type != MenuType.MAIN_MENU) { + try { + log.debug("Command execution yielded an error, returning to main menu"); + returnToRootMenu(); + } catch (Exception e) { + log.warn("Error returning to main menu, when trying to recover from command failure", e); + } + } + try { + PumpState pumpState = readPumpStateInternal(); + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) + .putCustomAttribute("exit", "2") + .putCustomAttribute("success", "true")); + return pumpState; + } catch (Exception e) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .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(); + } + } + + /** + * If there's an issue, this times out eventually and throws a CommandException + */ + private void ensureConnected() { + try { + if (isConnected()) { + return; + } + + boolean connectInitSuccessful = ruffyService.doRTConnect() == 0; + log.debug("Connect init successful: " + connectInitSuccessful); + log.debug("Waiting for first menu update to be sent"); + long timeoutExpired = System.currentTimeMillis() + 90 * 1000; + long initialUpdateTime = menuLastUpdated; + while (initialUpdateTime == menuLastUpdated) { + if (System.currentTimeMillis() > timeoutExpired) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboConnectTimeout") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION) + .putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : "")) + .putCustomAttribute("previousCommand", previousCommand)); + throw new CommandException("Timeout connecting to pump"); + } + SystemClock.sleep(50); + } + } catch (CommandException e) { + try { + ruffyService.doRTDisconnect(); + } catch (RemoteException e1) { + log.warn("Disconnect after connect failure failed", e1); + } + throw e; + } catch (Exception e) { + try { + ruffyService.doRTDisconnect(); + } catch (RemoteException e1) { + log.warn("Disconnect after connect failure failed", e1); + } + throw new CommandException("Unexpected exception while initiating/restoring pump connection", e); + } + } + + /** + * This reads the state of the pump, which is whatever is currently displayed on the display, + * no actions are performed. + */ + public PumpState readPumpStateInternal() { + PumpState state = new PumpState(); + state.timestamp = System.currentTimeMillis(); + Menu menu = currentMenu; + if (menu == null) { + log.debug("Returning empty PumpState, menu is unavailable"); + return state; + } + + log.debug("Parsing menu: " + menu); + MenuType menuType = menu.getType(); + state.menu = menuType.name(); + + if (menuType == MenuType.MAIN_MENU) { + Double tbrPercentage = (Double) menu.getAttribute(MenuAttribute.TBR); + BolusType bolusType = (BolusType) menu.getAttribute(MenuAttribute.BOLUS_TYPE); + Integer activeBasalRate = (Integer) menu.getAttribute(MenuAttribute.BASAL_SELECTED); + + if (!activeBasalRate.equals(1)) { + state.unsafeUsageDetected = PumpState.UNSUPPORTED_BASAL_RATE_PROFILE; + } else if (bolusType != null && bolusType != BolusType.NORMAL) { + state.unsafeUsageDetected = PumpState.UNSUPPORTED_BOLUS_TYPE; + } else if (tbrPercentage != null && tbrPercentage != 100) { + state.tbrActive = true; + Double displayedTbr = (Double) menu.getAttribute(MenuAttribute.TBR); + state.tbrPercent = displayedTbr.intValue(); + MenuTime durationMenuTime = ((MenuTime) menu.getAttribute(MenuAttribute.RUNTIME)); + state.tbrRemainingDuration = durationMenuTime.getHour() * 60 + durationMenuTime.getMinute(); + } + if (menu.attributes().contains(MenuAttribute.BASAL_RATE)) { + state.basalRate = ((double) menu.getAttribute(MenuAttribute.BASAL_RATE)); + } + if (menu.attributes().contains(MenuAttribute.BATTERY_STATE)) { + state.batteryState = ((int) menu.getAttribute(MenuAttribute.BATTERY_STATE)); + } + if (menu.attributes().contains(MenuAttribute.INSULIN_STATE)) { + state.insulinState = ((int) menu.getAttribute(MenuAttribute.INSULIN_STATE)); + } + if (menu.attributes().contains(MenuAttribute.TIME)) { + MenuTime pumpTime = (MenuTime) menu.getAttribute(MenuAttribute.TIME); + Date date = new Date(); + // infer yesterday as the pump's date if midnight just passed, but the pump is + // a bit behind + if (date.getHours() == 0 && date.getMinutes() <= 5 + && pumpTime.getHour() == 23 && pumpTime.getMinute() >= 55) { + date.setTime(date.getTime() - 24 * 60 * 60 * 1000); + } + date.setHours(pumpTime.getHour()); + date.setMinutes(pumpTime.getMinute()); + date.setSeconds(0); + state.pumpTime = date.getTime() - date.getTime() % 1000; + } + } else if (menuType == MenuType.WARNING_OR_ERROR) { + state.activeAlert = readWarningOrErrorCode(); + } else if (menuType == MenuType.STOP) { + state.suspended = true; + if (menu.attributes().contains(MenuAttribute.BATTERY_STATE)) { + state.batteryState = ((int) menu.getAttribute(MenuAttribute.BATTERY_STATE)); + } + if (menu.attributes().contains(MenuAttribute.INSULIN_STATE)) { + state.insulinState = ((int) menu.getAttribute(MenuAttribute.INSULIN_STATE)); + } + if (menu.attributes().contains(MenuAttribute.TIME)) { + MenuTime time = (MenuTime) menu.getAttribute(MenuAttribute.TIME); + Date date = new Date(); + date.setHours(time.getHour()); + date.setMinutes(time.getMinute()); + date.setSeconds(0); + state.pumpTime = date.getTime() - date.getTime() % 1000; + } + } + + log.debug("State read: " + state); + return state; + } + + @NonNull + public WarningOrErrorCode readWarningOrErrorCode() { + if (currentMenu == null || getCurrentMenu().getType() != MenuType.WARNING_OR_ERROR) { + return new WarningOrErrorCode(null, null, null); + } + Integer warningCode = (Integer) getCurrentMenu().getAttribute(MenuAttribute.WARNING); + Integer errorCode = (Integer) getCurrentMenu().getAttribute(MenuAttribute.ERROR); + int retries = 5; + while (warningCode == null && errorCode == null && retries > 0) { + waitForScreenUpdate(); + warningCode = (Integer) getCurrentMenu().getAttribute(MenuAttribute.WARNING); + errorCode = (Integer) getCurrentMenu().getAttribute(MenuAttribute.ERROR); + retries--; + } + String message = (String) getCurrentMenu().getAttribute(MenuAttribute.MESSAGE); + return new WarningOrErrorCode(warningCode, errorCode, message); + } + + public static class Key { + public static byte NO_KEY = (byte) 0x00; + public static byte MENU = (byte) 0x03; + public static byte CHECK = (byte) 0x0C; + public static byte UP = (byte) 0x30; + public static byte DOWN = (byte) 0xC0; + public static byte BACK = (byte) 0x33; + } + + // === pump ops === + @NonNull + public Menu getCurrentMenu() { + if (Thread.currentThread().isInterrupted()) + throw new CommandException("Interrupted"); + Menu menu = this.currentMenu; + if (menu == null) { + log.error("currentMenu == null, bailing"); + throw new CommandException("Unable to read current menu"); + } + return menu; + } + + @Nullable + private String getCurrentMenuName() { + Menu menu = this.currentMenu; + return menu != null ? menu.getType().toString() : ""; + } + + public void pressUpKey() { + log.debug("Pressing up key"); + pressKey(Key.UP); + log.debug("Releasing up key"); + } + + public void pressDownKey() { + log.debug("Pressing down key"); + pressKey(Key.DOWN); + log.debug("Releasing down key"); + } + + public void pressCheckKey() { + log.debug("Pressing check key"); + pressKey(Key.CHECK); + log.debug("Releasing check key"); + } + + public void pressMenuKey() { + log.debug("Pressing menu key"); + pressKey(Key.MENU); + log.debug("Releasing menu key"); + } + + private void pressBackKey() { + log.debug("Pressing back key"); + pressKey(Key.BACK); + log.debug("Releasing back key"); + } + + public void pressKeyMs(final byte key, long ms) { + long stepMs = 100; + try { + log.debug("Scroll: Pressing key for " + ms + " ms with step " + stepMs + " ms"); + ruffyService.rtSendKey(key, true); + ruffyService.rtSendKey(key, false); + while (ms > stepMs) { + SystemClock.sleep(stepMs); + ruffyService.rtSendKey(key, false); + ms -= stepMs; + } + SystemClock.sleep(ms); + ruffyService.rtSendKey(Key.NO_KEY, true); + log.debug("Releasing key"); + } catch (Exception e) { + throw new CommandException("Error while pressing buttons"); + } + } + + /** + * Wait until the menu is updated + */ + public void waitForScreenUpdate() { + if (Thread.currentThread().isInterrupted()) + throw new CommandException("Interrupted"); + synchronized (screenlock) { + try { + // updates usually come in every ~500, occasionally up to 1100ms + screenlock.wait((long) 2000); + } catch (InterruptedException e) { + throw new CommandException("Interrupted"); + } + } + } + + private void pressKey(final byte key) { + if (Thread.currentThread().isInterrupted()) + throw new CommandException("Interrupted"); + try { + ruffyService.rtSendKey(key, true); + SystemClock.sleep(150); + ruffyService.rtSendKey(Key.NO_KEY, true); + } catch (Exception e) { + throw new CommandException("Error while pressing buttons"); + } + } + + public void navigateToMenu(MenuType desiredMenu) { + verifyMenuIsDisplayed(MenuType.MAIN_MENU); + int moves = 20; + MenuType lastSeenMenu = getCurrentMenu().getType(); + while (lastSeenMenu != desiredMenu) { + log.debug("Navigating to menu " + desiredMenu + ", current menu: " + lastSeenMenu); + moves--; + if (moves == 0) { + throw new CommandException("Menu not found searching for " + desiredMenu + + ". Check menu settings on your pump to ensure it's not hidden."); + } + MenuType next = getCurrentMenu().getType(); + pressMenuKey(); + // sometimes the pump takes a bit longer (more than one screen refresh) to advance + // to the next menu. wait until we actually see the change to avoid overshoots. + while (next == lastSeenMenu) { + waitForScreenUpdate(); + next = getCurrentMenu().getType(); + } + lastSeenMenu = getCurrentMenu().getType(); + } + } + + /** + * Wait till a menu changed has completed, "away" from the menu provided as argument. + */ + public void waitForMenuToBeLeft(MenuType menuType) { + long timeout = System.currentTimeMillis() + 10 * 1000; + while (getCurrentMenu().getType() == menuType) { + if (System.currentTimeMillis() > timeout) { + throw new CommandException("Timeout waiting for menu " + menuType + " to be left"); + } + waitForScreenUpdate(); + } + } + + public void verifyMenuIsDisplayed(MenuType expectedMenu) { + verifyMenuIsDisplayed(expectedMenu, null); + } + + public void verifyMenuIsDisplayed(MenuType expectedMenu, String failureMessage) { + int attempts = 5; + while (getCurrentMenu().getType() != expectedMenu) { + attempts -= 1; + if (attempts > 0) { + waitForScreenUpdate(); + } else { + if (failureMessage == null) { + failureMessage = "Invalid pump state, expected to be in menu " + expectedMenu + ", but current menu is " + getCurrentMenuName(); + } + throw new CommandException(failureMessage); + } + } + } + + public void verifyRootMenuIsDisplayed() { + int retries = 600; + while (getCurrentMenu().getType() != MenuType.MAIN_MENU && getCurrentMenu().getType() != MenuType.STOP) { + if (retries > 0) { + SystemClock.sleep(100); + retries = retries - 1; + } else { + throw new CommandException("Invalid pump state, expected to be in menu MAIN or STOP but current menu is " + getCurrentMenuName()); + } + } + } + + @SuppressWarnings("unchecked") + public T readBlinkingValue(Class expectedType, MenuAttribute attribute) { + int retries = 5; + Object value = getCurrentMenu().getAttribute(attribute); + while (!expectedType.isInstance(value)) { + value = getCurrentMenu().getAttribute(attribute); + waitForScreenUpdate(); + retries--; + if (retries == 0) { + throw new CommandException("Failed to read blinkng value: " + attribute + "=" + value + " type=" + value); + } + } + return (T) value; + } + + @Override + public CommandResult deliverBolus(double amount, BolusProgressReporter bolusProgressReporter) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboBolusCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new BolusCommand(amount, bolusProgressReporter)); + } + + @Override + public void cancelBolus() { + if (activeCmd instanceof BolusCommand) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboBolusCmdCancel") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + ((BolusCommand) activeCmd).requestCancellation(); + } else { + log.error("cancelBolus called, but active command is not a bolus:" + activeCmd); + } + } + + @Override + public CommandResult setTbr(int percent, int duration) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboSetTbrCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new SetTbrCommand(percent, duration)); + } + + @Override + public CommandResult cancelTbr() { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboCancelTbrCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new CancelTbrCommand()); + } + + @Override + public CommandResult confirmAlert(int warningCode) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboConfirmAlertCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new ConfirmAlertCommand(warningCode)); + } + + @Override + public CommandResult readHistory(PumpHistoryRequest request) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboReadHistoryCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new ReadHistoryCommand(request)); + } + + @Override + public CommandResult readBasalProfile() { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboReadBasalProfileCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new ReadBasalProfileCommand()); + } + + @Override + public CommandResult setBasalProfile(BasalProfile basalProfile) { + FabricPrivacy.getInstance().logCustom(new CustomEvent("ComboSetBasalProfileCmd") + .putCustomAttribute("buildversion", BuildConfig.BUILDVERSION) + .putCustomAttribute("version", BuildConfig.VERSION)); + return runCommand(new SetBasalProfileCommand(basalProfile)); + } + + @Override + public CommandResult getDateAndTime() { + throw new RuntimeException("Not supported"); + } + + @Override + public CommandResult setDateAndTime() { + throw new RuntimeException("Not supported"); + } + + /** + * Confirms and dismisses the given alert if it's raised before the timeout + */ + public boolean confirmAlert(@NonNull Integer warningCode, int maxWaitMs) { + long timeout = System.currentTimeMillis() + maxWaitMs; + while (System.currentTimeMillis() < timeout) { + if (getCurrentMenu().getType() == MenuType.WARNING_OR_ERROR) { + WarningOrErrorCode warningOrErrorCode = readWarningOrErrorCode(); + if (warningOrErrorCode.errorCode != null) { + throw new CommandException("Pump is in error state"); + } + Integer displayedWarningCode = warningOrErrorCode.warningCode; + String errorMsg = null; + try { + errorMsg = (String) getCurrentMenu().getAttribute(MenuAttribute.MESSAGE); + } catch (Exception e) { + // ignore + } + if (!Objects.equals(displayedWarningCode, warningCode)) { + throw new CommandException("An alert other than the expected warning " + warningCode + " was raised by the pump: " + + displayedWarningCode + "(" + errorMsg + "). Please check the pump."); + } + + // confirm alert + verifyMenuIsDisplayed(MenuType.WARNING_OR_ERROR); + pressCheckKey(); + // dismiss alert + // if the user has confirmed the alert we have dismissed it with the button press + // above already, so only do that if an alert is still displayed + waitForScreenUpdate(); + if (getCurrentMenu().getType() == MenuType.WARNING_OR_ERROR) { + pressCheckKey(); + } + // wait till the pump has processed the alarm, otherwise it might still be showing + // when a command returns + WarningOrErrorCode displayedWarning = readWarningOrErrorCode(); + while (Objects.equals(displayedWarning.warningCode, warningCode)) { + waitForScreenUpdate(); + displayedWarning = readWarningOrErrorCode(); + } + return true; + } + SystemClock.sleep(10); + } + return false; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/WarningOrErrorCode.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/WarningOrErrorCode.java new file mode 100644 index 0000000000..a4e21da289 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/WarningOrErrorCode.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter; + +import android.support.annotation.Nullable; + +public class WarningOrErrorCode { + @Nullable + public final Integer warningCode; + @Nullable + public final Integer errorCode; + @Nullable + public String message; + + public WarningOrErrorCode(@Nullable Integer warningCode, @Nullable Integer errorCode, @Nullable String message) { + this.warningCode = warningCode; + this.errorCode = errorCode; + this.message = message; + } + + @Override + public String toString() { + return "WarningOrErrorCode{" + + "warningCode=" + warningCode + + ", errorCode=" + errorCode + + ", message=" + message + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/BaseCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/BaseCommand.java new file mode 100644 index 0000000000..5cc3e98b23 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/BaseCommand.java @@ -0,0 +1,85 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +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 java.util.Calendar; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.CommandResult; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpWarningCodes; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.RuffyScripter; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; + +public abstract class BaseCommand implements Command { + // RS will inject itself here + protected RuffyScripter scripter; + + protected CommandResult result; + + public BaseCommand() { + result = new CommandResult(); + } + + @Override + public void setScripter(RuffyScripter scripter) { + this.scripter = scripter; + } + + @Override + public boolean needsRunMode() { + return true; + } + + /** + * A warning id (or null) caused by a disconnect we can safely confirm on reconnect, + * knowing it's not severe as it was caused by this command. + * @see PumpWarningCodes + */ + @Override + public Integer getReconnectWarningId() { + return null; + } + + @Override + public List validateArguments() { + return Collections.emptyList(); + } + + @Override + public CommandResult getResult() { + return result; + } + + @NonNull + protected Bolus readBolusRecord() { + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA); + BolusType bolusType = (BolusType) scripter.getCurrentMenu().getAttribute(MenuAttribute.BOLUS_TYPE); + boolean isValid = bolusType == BolusType.NORMAL; + Double bolus = (Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.BOLUS); + long recordDate = readRecordDate(); + return new Bolus(recordDate, bolus, isValid); + } + + protected long readRecordDate() { + MenuDate date = (MenuDate) scripter.getCurrentMenu().getAttribute(MenuAttribute.DATE); + MenuTime time = (MenuTime) scripter.getCurrentMenu().getAttribute(MenuAttribute.TIME); + + int year = Calendar.getInstance().get(Calendar.YEAR); + if (date.getMonth() > Calendar.getInstance().get(Calendar.MONTH) + 1) { + year -= 1; + } + Calendar calendar = Calendar.getInstance(); + calendar.set(year, date.getMonth() - 1, date.getDay(), time.getHour(), time.getMinute(), 0); + + // round to second + return calendar.getTimeInMillis() - calendar.getTimeInMillis() % 1000; + + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/BolusCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/BolusCommand.java new file mode 100644 index 0000000000..4188716553 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/BolusCommand.java @@ -0,0 +1,254 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +import android.os.SystemClock; + +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.List; +import java.util.Objects; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BolusProgressReporter; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.CommandResult; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpWarningCodes; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.WarningOrErrorCode; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.RuffyScripter; + +import static info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BolusProgressReporter.State.DELIVERED; +import static info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BolusProgressReporter.State.DELIVERING; +import static info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BolusProgressReporter.State.PROGRAMMING; +import static info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BolusProgressReporter.State.STOPPED; +import static info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BolusProgressReporter.State.STOPPING; + +public class BolusCommand extends BaseCommand { + private static final Logger log = LoggerFactory.getLogger(BolusCommand.class); + + protected final double bolus; + private final BolusProgressReporter bolusProgressReporter; + private volatile boolean cancelRequested; + + public BolusCommand(double bolus, BolusProgressReporter bolusProgressReporter) { + this.bolus = bolus; + this.bolusProgressReporter = bolusProgressReporter; + } + + @Override + public List validateArguments() { + List violations = new ArrayList<>(); + + if (bolus <= 0) { + violations.add("Requested bolus non-positive: " + bolus); + } + + return violations; + } + + @Override + public Integer getReconnectWarningId() { + return PumpWarningCodes.BOLUS_CANCELLED; + } + + @Override + public void execute() { + if (cancelRequested) { + bolusProgressReporter.report(STOPPED, 0, 0); + result.success = true; + log.debug("Stage 0: cancelled bolus before programming"); + return; + } + + bolusProgressReporter.report(PROGRAMMING, 0, 0); + enterBolusMenu(); + inputBolusAmount(); + verifyDisplayedBolusAmount(); + + // last chance to abort before confirming the bolus + if (cancelRequested) { + bolusProgressReporter.report(STOPPING, 0, 0); + scripter.returnToRootMenu(); + bolusProgressReporter.report(STOPPED, 0, 0); + result.success = true; + log.debug("Stage 1: cancelled bolus after programming"); + return; + } + + // confirm bolus + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER); + scripter.pressCheckKey(); + log.debug("Stage 2: bolus confirmed"); + + // the pump displays the entered bolus and waits a few seconds to let user check and cancel + while (scripter.getCurrentMenu().getType() == MenuType.BOLUS_ENTER) { + if (cancelRequested) { + log.debug("Stage 2: cancelling during confirmation wait"); + bolusProgressReporter.report(STOPPING, 0, 0); + scripter.pressUpKey(); + // wait up to 1s for a BOLUS_CANCELLED alert, if it doesn't happen we missed + // the window, simply continue and let the next cancel attempt try its luck + boolean alertWasCancelled = scripter.confirmAlert(PumpWarningCodes.BOLUS_CANCELLED, 1000); + if (alertWasCancelled) { + log.debug("Stage 2: successfully cancelled during confirmation wait"); + bolusProgressReporter.report(STOPPED, 0, 0); + result.success = true; + return; + } + } + SystemClock.sleep(10); + } + + // the bolus progress is displayed on the main menu + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU, + "Pump did not return to MAIN_MEU from BOLUS_ENTER to deliver bolus. " + + "Check pump manually, the bolus might not have been delivered."); + bolusProgressReporter.report(DELIVERING, 0, 0); + + // wait for bolus delivery to complete; the remaining units to deliver are counted down + boolean cancelInProgress = false; + Double lastBolusReported = 0d; + Double bolusRemaining = (Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.BOLUS_REMAINING); + Thread cancellationThread = null; + while (bolusRemaining != null || scripter.getCurrentMenu().getType() == MenuType.WARNING_OR_ERROR) { + if (cancelRequested && !cancelInProgress) { + log.debug("Stage 3: cancellation while delivering bolus"); + bolusProgressReporter.report(STOPPING, 0, 0); + cancelInProgress = true; + cancellationThread = new Thread(() -> + scripter.pressKeyMs(RuffyScripter.Key.UP, 3000), "bolus-canceller"); + cancellationThread.start(); + } + if (scripter.getCurrentMenu().getType() == MenuType.WARNING_OR_ERROR) { + // confirm warning alert and update the result to indicate alerts occurred + WarningOrErrorCode warningOrErrorCode = scripter.readWarningOrErrorCode(); + if (warningOrErrorCode.errorCode != null) { + throw new CommandException("Pump is in error state"); + } + Integer warningCode = warningOrErrorCode.warningCode; + if (Objects.equals(warningCode, PumpWarningCodes.BOLUS_CANCELLED)) { + // wait until cancellation thread releases the up button, otherwise we won't + // be able to confirm anything + if (cancellationThread != null) { + try { + cancellationThread.join(3500); + } catch (InterruptedException e) { + // ignore + } + } + scripter.confirmAlert(PumpWarningCodes.BOLUS_CANCELLED, 2000); + bolusProgressReporter.report(STOPPED, 0, 0); + log.debug("Stage 3: confirmed BOLUS CANCELLED after cancelling bolus during delivery"); + } else if (Objects.equals(warningCode, PumpWarningCodes.CARTRIDGE_LOW)) { + scripter.confirmAlert(PumpWarningCodes.CARTRIDGE_LOW, 2000); + result.forwardedWarnings.add(PumpWarningCodes.CARTRIDGE_LOW); + log.debug("Stage 3: confirmed low cartridge alert and forwarding to AAPS"); + } else if (Objects.equals(warningCode, PumpWarningCodes.BATTERY_LOW)) { + scripter.confirmAlert(PumpWarningCodes.BATTERY_LOW, 2000); + result.forwardedWarnings.add(PumpWarningCodes.BATTERY_LOW); + log.debug("Stage 3: confirmed low battery alert and forwarding to AAPS"); + } else { + // all other warnings or errors; + // An occlusion error can also occur during bolus. To read the partially delivered + // bolus, we'd have to first confirm the error. But an (occlusion) **error** shall not + // be confirmed and potentially be swallowed by a bug or shaky comms, so we let + // the pump be noisy (which the user will have to interact with anyway). + // Thus, this method will terminate with an exception and display an error message. + // Ideally, sometime after the user has dealt with the situation, the partially + // delivered bolus should be read. However, ready history is tricky at this point. + // Also: with an occlusion, the amount of insulin active is in question. + // It would be safer to assume the delivered bolus results in IOB, but there's + // only so much we can do at this point, so the user shall take over here and + // add a bolus record as and if needed. + throw new CommandException("Pump is showing exotic warning/error: " + warningOrErrorCode); + } + } + if (bolusRemaining != null && !Objects.equals(bolusRemaining, lastBolusReported)) { + log.debug("Delivering bolus, remaining: " + bolusRemaining); + int percentDelivered = (int) (100 - (bolusRemaining / bolus * 100)); + bolusProgressReporter.report(DELIVERING, percentDelivered, bolus - bolusRemaining); + lastBolusReported = bolusRemaining; + } + SystemClock.sleep(50); + bolusRemaining = (Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.BOLUS_REMAINING); + } + // if a cancellation was started by pressing up for 3 seconds but the bolus has finished during those + // three seconds, must wait until the button is unpressed again so that follow up commands + // work properly. + if (cancellationThread != null) { + try { + cancellationThread.join(); + } catch (InterruptedException e) { + // ignore + } + } + + if (cancelInProgress) { + log.debug("Stage 4: bolus was cancelled, with unknown amount delivered"); + } else { + log.debug("Stage 4: full bolus of " + bolus + " U was successfully delivered"); + bolusProgressReporter.report(DELIVERED, 100, bolus); + } + result.success = true; + } + + private void enterBolusMenu() { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + scripter.navigateToMenu(MenuType.BOLUS_MENU); + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_MENU); + scripter.pressCheckKey(); + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER); + } + + private void inputBolusAmount() { + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER); + // press 'up' once for each 0.1 U increment + long steps = Math.round(bolus * 10); + for (int i = 0; i < steps; i++) { + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER); + scripter.pressUpKey(); + SystemClock.sleep(50); + } + } + + private void verifyDisplayedBolusAmount() { + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER); + + // wait up to 10s for any scrolling to finish + double displayedBolus = scripter.readBlinkingValue(Double.class, MenuAttribute.BOLUS); + long timeout = System.currentTimeMillis() + 10 * 1000; + while (timeout > System.currentTimeMillis() && bolus - displayedBolus > 0.05) { + log.debug("Waiting for pump to process scrolling input for amount, current: " + displayedBolus + ", desired: " + bolus); + SystemClock.sleep(50); + displayedBolus = scripter.readBlinkingValue(Double.class, MenuAttribute.BOLUS); + } + + log.debug("Final bolus: " + displayedBolus); + if (Math.abs(displayedBolus - bolus) > 0.01) { + throw new CommandException("Failed to set correct bolus. Expected: " + bolus + ", actual: " + displayedBolus); + } + + // check again to ensure the displayed value hasn't change due to due scrolling taking extremely long + SystemClock.sleep(1000); + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_ENTER); + double refreshedDisplayedBolus = scripter.readBlinkingValue(Double.class, MenuAttribute.BOLUS); + if (Math.abs(displayedBolus - refreshedDisplayedBolus) > 0.01) { + throw new CommandException("Failed to set bolus: bolus changed after input stopped from " + + displayedBolus + " -> " + refreshedDisplayedBolus); + } + } + + public void requestCancellation() { + log.debug("Bolus cancellation requested"); + cancelRequested = true; + bolusProgressReporter.report(STOPPING, 0, 0); + } + + @Override + public String toString() { + return "BolusCommand{" + + "bolus=" + bolus + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/CancelTbrCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/CancelTbrCommand.java new file mode 100644 index 0000000000..b1787005e1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/CancelTbrCommand.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +import org.monkey.d.ruffy.ruffy.driver.display.MenuType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpState; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpWarningCodes; + +public class CancelTbrCommand extends BaseCommand { + private static final Logger log = LoggerFactory.getLogger(CancelTbrCommand.class); + + @Override + public Integer getReconnectWarningId() { + return PumpWarningCodes.TBR_CANCELLED; + } + + @Override + public void execute() { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + PumpState pumpState = scripter.readPumpStateInternal(); + if (!pumpState.tbrActive) { + // This is non-critical; when cancelling a TBR and the connection was interrupted + // the TBR was cancelled by that. In that case not cancelling anything is fine. + result.success = true; + return; + } + + log.debug("Cancelling active TBR of " + pumpState.tbrPercent + + "% with " + pumpState.tbrRemainingDuration + " min remaining"); + SetTbrCommand setTbrCommand = new SetTbrCommand(100, 0); + setTbrCommand.setScripter(scripter); + setTbrCommand.execute(); + result = setTbrCommand.result; + } + + @Override + public String toString() { + return "CancelTbrCommand{}"; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/Command.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/Command.java new file mode 100644 index 0000000000..152befe00d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/Command.java @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +import java.util.List; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.RuffyScripter; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.CommandResult; + +/** + * Interface for all commands to be executed by the pump. + *

+ * Note on cammond methods and timing: a method shall wait before and after executing + * as necessary to not cause timing issues, so the caller can just call methods in + * sequence, letting the methods take care of waits. + */ +public interface Command { + void setScripter(RuffyScripter scripter); + List validateArguments(); + boolean needsRunMode(); + void execute(); + CommandResult getResult(); + Integer getReconnectWarningId(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/CommandException.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/CommandException.java new file mode 100644 index 0000000000..77c1db5437 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/CommandException.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +public class CommandException extends RuntimeException { + public CommandException(String message) { + super(message); + } + + public CommandException(String message, Exception exception) { + super(message, exception); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ConfirmAlertCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ConfirmAlertCommand.java new file mode 100644 index 0000000000..636b6a624d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ConfirmAlertCommand.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +public class ConfirmAlertCommand extends BaseCommand { + private final int warningCode; + + public ConfirmAlertCommand(int warningCode) { + this.warningCode = warningCode; + } + + @Override + public void execute() { + result.success(scripter.confirmAlert(warningCode, 5000)); + } + + @Override + public boolean needsRunMode() { + return false; + } + + @Override + public String toString() { + return "ConfirmAlertCommand{" + + "warningCode=" + warningCode + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadBasalProfileCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadBasalProfileCommand.java new file mode 100644 index 0000000000..2c23d5814b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadBasalProfileCommand.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +import org.monkey.d.ruffy.ruffy.driver.display.Menu; +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.MenuTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BasalProfile; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpState; + +public class ReadBasalProfileCommand extends BaseCommand { + private static final Logger log = LoggerFactory.getLogger(ReadBasalProfileCommand.class); + + @Override + public void execute() { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + if (scripter.readPumpStateInternal().unsafeUsageDetected == PumpState.UNSUPPORTED_BASAL_RATE_PROFILE) { + throw new CommandException("Active basal rate profile != 1"); + } + scripter.navigateToMenu(MenuType.BASAL_1_MENU); + scripter.verifyMenuIsDisplayed(MenuType.BASAL_1_MENU); + scripter.pressCheckKey(); + + BasalProfile basalProfile = new BasalProfile(); + + // summary screen is shown; press menu to page through hours, wraps around to summary; + scripter.verifyMenuIsDisplayed(MenuType.BASAL_TOTAL); + for (int i = 0; i < 24; i++) { + scripter.pressMenuKey(); + Menu menu = scripter.getCurrentMenu(); + while (menu.getType() != MenuType.BASAL_SET + || ((MenuTime) scripter.getCurrentMenu().getAttribute(MenuAttribute.BASAL_START)).getHour() != i) { + scripter.waitForScreenUpdate(); + menu = scripter.getCurrentMenu(); + } + scripter.verifyMenuIsDisplayed(MenuType.BASAL_SET); + + MenuTime startTime = (MenuTime) scripter.getCurrentMenu().getAttribute(MenuAttribute.BASAL_START); + if (i != startTime.getHour()) { + throw new CommandException("Attempting to read basal rate for hour " + i + ", but hour " + startTime.getHour() + " is displayed"); + } + basalProfile.hourlyRates[i] = scripter.readBlinkingValue(Double.class, MenuAttribute.BASAL_RATE); + log.debug("Read basal profile, hour " + i + ": " + basalProfile.hourlyRates[i]); + } + + log.debug("Basal profile read: " + Arrays.toString(basalProfile.hourlyRates)); + + scripter.returnToRootMenu(); + scripter.verifyRootMenuIsDisplayed(); + + result.success(true).basalProfile(basalProfile); + } + + @Override + public String toString() { + return "ReadBasalProfileCommand{}"; + } +} 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 new file mode 100644 index 0000000000..6af1b99dee --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadHistoryCommand.java @@ -0,0 +1,274 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +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.MenuDate; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.Date; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpAlert; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistory; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistoryRequest; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Tbr; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Tdd; + +public class ReadHistoryCommand extends BaseCommand { + private static Logger log = LoggerFactory.getLogger(ReadHistoryCommand.class); + + private final PumpHistoryRequest request; + private final PumpHistory history = new PumpHistory(); + + public ReadHistoryCommand(PumpHistoryRequest request) { + this.request = request; + } + + @Override + public void execute() { + if (request.bolusHistory == PumpHistoryRequest.SKIP + && request.tbrHistory == PumpHistoryRequest.SKIP + && request.pumpErrorHistory == PumpHistoryRequest.SKIP + && request.tddHistory == PumpHistoryRequest.SKIP) { + throw new CommandException("History request but all data types are skipped"); + } + + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + scripter.navigateToMenu(MenuType.MY_DATA_MENU); + scripter.verifyMenuIsDisplayed(MenuType.MY_DATA_MENU); + scripter.pressCheckKey(); + + // bolus history + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA); + if (request.bolusHistory != PumpHistoryRequest.SKIP) { + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + if (totalRecords > 0) { + if (request.bolusHistory == PumpHistoryRequest.LAST) { + Bolus bolus = readBolusRecord(); + history.bolusHistory.add(bolus); + } else { + readBolusRecords(request.bolusHistory); + } + } + } + + if (request.pumpErrorHistory != PumpHistoryRequest.SKIP + || request.tddHistory != PumpHistoryRequest.SKIP + || request.tbrHistory != PumpHistoryRequest.SKIP) { + // error history + scripter.pressMenuKey(); + scripter.verifyMenuIsDisplayed(MenuType.ERROR_DATA); + if (request.pumpErrorHistory != PumpHistoryRequest.SKIP) { + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + if (totalRecords > 0) { + if (request.pumpErrorHistory == PumpHistoryRequest.LAST) { + PumpAlert error = readAlertRecord(); + history.pumpAlertHistory.add(error); + } else { + readAlertRecords(request.pumpErrorHistory); + } + } + } + + // tdd history (TBRs are added to history only after they've completed running) + scripter.pressMenuKey(); + scripter.verifyMenuIsDisplayed(MenuType.DAILY_DATA); + if (request.tddHistory != PumpHistoryRequest.SKIP) { + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + if (totalRecords > 0) { + if (request.tddHistory == PumpHistoryRequest.LAST) { + Tdd tdd = readTddRecord(); + history.tddHistory.add(tdd); + } else { + readTddRecords(request.tbrHistory); + } + } + } + + // tbr history + scripter.pressMenuKey(); + scripter.verifyMenuIsDisplayed(MenuType.TBR_DATA); + if (request.tbrHistory != PumpHistoryRequest.SKIP) { + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + if (totalRecords > 0) { + if (request.tbrHistory == PumpHistoryRequest.LAST) { + Tbr tbr = readTbrRecord(); + history.tbrHistory.add(tbr); + } else { + readTbrRecords(request.tbrHistory); + } + } + } + } + + if (log.isDebugEnabled()) { + if (!history.bolusHistory.isEmpty()) { + log.debug("Read bolus history (" + history.bolusHistory.size() + "):"); + for (Bolus bolus : history.bolusHistory) { + log.debug(new Date(bolus.timestamp) + ": " + bolus.toString()); + } + } + if (!history.pumpAlertHistory.isEmpty()) { + log.debug("Read error history (" + history.pumpAlertHistory.size() + "):"); + for (PumpAlert pumpAlert : history.pumpAlertHistory) { + log.debug(new Date(pumpAlert.timestamp) + ": " + pumpAlert.toString()); + } + } + if (!history.tddHistory.isEmpty()) { + log.debug("Read TDD history (" + history.tddHistory.size() + "):"); + for (Tdd tdd : history.tddHistory) { + log.debug(new Date(tdd.timestamp) + ": " + tdd.toString()); + } + } + if (!history.tbrHistory.isEmpty()) { + log.debug("Read TBR history (" + history.tbrHistory.size() + "):"); + for (Tbr tbr : history.tbrHistory) { + log.debug(new Date(tbr.timestamp) + ": " + tbr.toString()); + } + } + } + + scripter.returnToRootMenu(); + scripter.verifyRootMenuIsDisplayed(); + + result.success(true).history(history); + } + + private void readTddRecords(long requestedTime) { + int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + while (true) { + Tdd tdd = readTddRecord(); + if (requestedTime != PumpHistoryRequest.FULL && tdd.timestamp < requestedTime) { + break; + } + log.debug("Read TDD record #" + record + "/" + totalRecords); + history.tddHistory.add(tdd); + log.debug("Parsed " + scripter.getCurrentMenu().toString() + " => " + tdd); + if (record == totalRecords) { + break; + } + scripter.pressDownKey(); + while (record == (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD)) { + scripter.waitForScreenUpdate(); + } + record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + } + } + + @NonNull + private Tdd readTddRecord() { + scripter.verifyMenuIsDisplayed(MenuType.DAILY_DATA); + Double dailyTotal = (Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.DAILY_TOTAL); + MenuDate date = (MenuDate) scripter.getCurrentMenu().getAttribute(MenuAttribute.DATE); + + int year = Calendar.getInstance().get(Calendar.YEAR); + if (date.getMonth() > Calendar.getInstance().get(Calendar.MONTH) + 1) { + year -= 1; + } + Calendar calendar = Calendar.getInstance(); + calendar.set(year, date.getMonth() - 1, date.getDay(), 0, 0, 0); + + return new Tdd(calendar.getTimeInMillis(), dailyTotal); + } + + private void readTbrRecords(long requestedTime) { + int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + while (true) { + Tbr tbr = readTbrRecord(); + if (requestedTime != PumpHistoryRequest.FULL && tbr.timestamp < requestedTime) { + break; + } + log.debug("Read TBR record #" + record + "/" + totalRecords); + history.tbrHistory.add(tbr); + log.debug("Parsed " + scripter.getCurrentMenu().toString() + " => " + tbr); + if (record == totalRecords) { + break; + } + scripter.pressDownKey(); + while (record == (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD)) { + scripter.waitForScreenUpdate(); + } + record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + } + } + + @NonNull + private Tbr readTbrRecord() { + scripter.verifyMenuIsDisplayed(MenuType.TBR_DATA); + Double percentage = (Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.TBR); + MenuTime durationTime = (MenuTime) scripter.getCurrentMenu().getAttribute(MenuAttribute.RUNTIME); + int duration = durationTime.getHour() * 60 + durationTime.getMinute(); + long tbrStartDate = readRecordDate() - duration * 60 * 1000; + return new Tbr(tbrStartDate, duration, percentage.intValue()); + } + + private void readBolusRecords(long requestedTime) { + int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + while (true) { + Bolus bolus = readBolusRecord(); + if (requestedTime != PumpHistoryRequest.FULL && bolus.timestamp < requestedTime) { + break; + } + log.debug("Read bolus record #" + record + "/" + totalRecords); + history.bolusHistory.add(bolus); + log.debug("Parsed " + scripter.getCurrentMenu().toString() + " => " + bolus); + if (record == totalRecords) { + break; + } + scripter.pressDownKey(); + while (record == (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD)) { + scripter.waitForScreenUpdate(); + } + record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + } + } + + private void readAlertRecords(long requestedTime) { + int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD); + while (true) { + PumpAlert error = readAlertRecord(); + if (requestedTime != PumpHistoryRequest.FULL && error.timestamp < requestedTime) { + break; + } + log.debug("Read alert record #" + record + "/" + totalRecords); + history.pumpAlertHistory.add(error); + log.debug("Parsed " + scripter.getCurrentMenu().toString() + " => " + error); + if (record == totalRecords) { + break; + } + scripter.pressDownKey(); + while (record == (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD)) { + scripter.waitForScreenUpdate(); + } + record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD); + } + } + + @NonNull + private PumpAlert readAlertRecord() { + scripter.verifyMenuIsDisplayed(MenuType.ERROR_DATA); + + Integer warningCode = (Integer) scripter.getCurrentMenu().getAttribute(MenuAttribute.WARNING); + Integer errorCode = (Integer) scripter.getCurrentMenu().getAttribute(MenuAttribute.ERROR); + String message = (String) scripter.getCurrentMenu().getAttribute(MenuAttribute.MESSAGE); + long recordDate = readRecordDate(); + return new PumpAlert(recordDate, warningCode, errorCode, message); + } + + @Override + public String toString() { + return "ReadHistoryCommand{" + + "request=" + request + + ", history=" + history + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadPumpStateCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadPumpStateCommand.java new file mode 100644 index 0000000000..cfa8d0f329 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadPumpStateCommand.java @@ -0,0 +1,19 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +public class ReadPumpStateCommand extends BaseCommand { + @Override + public void execute() { + // nothing to do, scripter adds state to all command results + result.success = true; + } + + @Override + public String toString() { + return "ReadPumpStateCommand{}"; + } + + @Override + public boolean needsRunMode() { + return false; + } +} 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 new file mode 100644 index 0000000000..e55fb4d010 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/ReadQuickInfoCommand.java @@ -0,0 +1,76 @@ +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(); + scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA); + List 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; + } + + @Override + public boolean needsRunMode() { + return false; + } + + @Override + public String toString() { + return "ReadQuickInfoCommand{}"; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/SetBasalProfileCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/SetBasalProfileCommand.java new file mode 100644 index 0000000000..f403c2a89b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/SetBasalProfileCommand.java @@ -0,0 +1,162 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +import android.os.SystemClock; + +import org.monkey.d.ruffy.ruffy.driver.display.Menu; +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.MenuTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.BasalProfile; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpState; + +public class SetBasalProfileCommand extends BaseCommand { + private static final Logger log = LoggerFactory.getLogger(SetBasalProfileCommand.class); + + private final BasalProfile basalProfile; + + public SetBasalProfileCommand(BasalProfile basalProfile) { + this.basalProfile = basalProfile; + } + + @Override + public void execute() { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + if (scripter.readPumpStateInternal().unsafeUsageDetected == PumpState.UNSUPPORTED_BASAL_RATE_PROFILE) { + throw new CommandException("Active basal rate profile != 1"); + } + scripter.navigateToMenu(MenuType.BASAL_1_MENU); + scripter.verifyMenuIsDisplayed(MenuType.BASAL_1_MENU); + scripter.pressCheckKey(); + + // summary screen is shown; press menu to page through hours, wraps around to summary; + scripter.verifyMenuIsDisplayed(MenuType.BASAL_TOTAL); + for (int i = 0; i < 24; i++) { + scripter.pressMenuKey(); + Menu menu = scripter.getCurrentMenu(); + while (menu.getType() != MenuType.BASAL_SET + || ((MenuTime) scripter.getCurrentMenu().getAttribute(MenuAttribute.BASAL_START)).getHour() != i) { + scripter.waitForScreenUpdate(); + menu = scripter.getCurrentMenu(); + } + scripter.verifyMenuIsDisplayed(MenuType.BASAL_SET); + + double requestedRate = basalProfile.hourlyRates[i]; + long change = inputBasalRate(requestedRate); + if (change != 0) { + verifyDisplayedRate(requestedRate, change); + } + + log.debug("Set basal profile, hour " + i + ": " + requestedRate); + } + + // move from hourly values to basal total + scripter.pressCheckKey(); + scripter.verifyMenuIsDisplayed(MenuType.BASAL_TOTAL); + + // check total basal total on pump matches requested total + Double pumpTotal = (Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.BASAL_TOTAL); + Double requestedTotal = 0d; + for (int i = 0; i < 24; i++) { + requestedTotal += basalProfile.hourlyRates[i]; + } + if (Math.abs(pumpTotal - requestedTotal) > 0.001) { + throw new CommandException("Basal total of " + pumpTotal + " differs from requested total of " + requestedTotal); + } + + // confirm entered basal rate + scripter.pressCheckKey(); + scripter.verifyRootMenuIsDisplayed(); + + result.success(true).basalProfile(basalProfile); + } + + private long inputBasalRate(double requestedRate) { + double currentRate = scripter.readBlinkingValue(Double.class, MenuAttribute.BASAL_RATE); + log.debug("Current rate: " + currentRate + ", requested: " + requestedRate); + // the pump changes steps size from 0.01 to 0.05 when crossing 1.00 U + long steps = 0; + if (currentRate == 0) { + // edge case of starting from 0.00; + steps = stepsToOne(0.05) - stepsToOne(requestedRate) + 1; + } else { + steps = stepsToOne(currentRate) - stepsToOne(requestedRate); + } + if (steps == 0) { + return 0; + } + log.debug("Pressing " + (steps > 0 ? "up" : "down") + " " + Math.abs(steps) + " times"); + for (int i = 0; i < Math.abs(steps); i++) { + scripter.verifyMenuIsDisplayed(MenuType.BASAL_SET); + log.debug("Push #" + (i + 1) + "/" + Math.abs(steps)); + if (steps > 0) scripter.pressUpKey(); + else scripter.pressDownKey(); + SystemClock.sleep(50); + } + return steps; + } + + /** + * Steps required to go up to 1.0 (positive return value), + * or down to 1.0 (negative return value). + */ + private long stepsToOne(double rate) { + double change = (1.0 - rate); + if (rate > 1) return Math.round(change / 0.05); + return Math.round(change / 0.01); + } + + private void verifyDisplayedRate(double requestedRate, long change) { + scripter.verifyMenuIsDisplayed(MenuType.BASAL_SET); + // wait up to 5s for any scrolling to finish + double displayedRate = scripter.readBlinkingValue(Double.class, MenuAttribute.BASAL_RATE); + long timeout = System.currentTimeMillis() + 10 * 1000; + while (timeout > System.currentTimeMillis() + && ((change > 0 && requestedRate - displayedRate > 0.001) // displayedRate < requestedRate) + || (change < 0 && displayedRate - requestedRate > 0.001))) { //displayedRate > requestedRate))) { + log.debug("Waiting for pump to process scrolling input for rate, current: " + + displayedRate + ", desired: " + requestedRate + ", scrolling " + + (change > 0 ? "up" : "down")); + scripter.waitForScreenUpdate(); + displayedRate = scripter.readBlinkingValue(Double.class, MenuAttribute.BASAL_RATE); + } + log.debug("Final displayed basal rate: " + displayedRate); + if (Math.abs(displayedRate - requestedRate) > 0.001) { + throw new CommandException("Failed to set basal rate, requested: " + + requestedRate + ", actual: " + displayedRate); + } + + // check again to ensure the displayed value hasn't change and scrolled past the desired + // value due to due scrolling taking extremely long + SystemClock.sleep(1000); + scripter.verifyMenuIsDisplayed(MenuType.BASAL_SET); + double refreshedDisplayedRate = scripter.readBlinkingValue(Double.class, MenuAttribute.BASAL_RATE); + if (Math.abs(displayedRate - refreshedDisplayedRate) > 0.001) { + throw new CommandException("Failed to set basal rate: " + + "percentage changed after input stopped from " + + displayedRate + " -> " + refreshedDisplayedRate); + } + } + + @Override + public List validateArguments() { + ArrayList violations = new ArrayList<>(); + if (basalProfile == null) { + violations.add("No basal profile supplied"); + } + + return violations; + } + + @Override + public String toString() { + return "SetBasalProfileCommand{" + + "basalProfile=" + basalProfile + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/SetTbrCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/SetTbrCommand.java new file mode 100644 index 0000000000..f618d50156 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/commands/SetTbrCommand.java @@ -0,0 +1,286 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands; + +import android.os.SystemClock; + +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.MenuTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpState; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.PumpWarningCodes; +import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.WarningOrErrorCode; + +public class SetTbrCommand extends BaseCommand { + private static final Logger log = LoggerFactory.getLogger(SetTbrCommand.class); + + private final long percentage; + private final long duration; + + public SetTbrCommand(long percentage, long duration) { + this.percentage = percentage; + this.duration = duration; + } + + @Override + public List validateArguments() { + List violations = new ArrayList<>(); + + if (percentage % 10 != 0) { + violations.add("TBR percentage must be set in 10% steps"); + } + if (percentage < 0 || percentage > 500) { + violations.add("TBR percentage must be within 0-500%"); + } + + if (percentage != 100) { + if (duration % 15 != 0) { + violations.add("TBR duration can only be set in 15 minute steps"); + } + if (duration > 60 * 24) { + violations.add("Maximum TBR duration is 24 hours"); + } + } + + return violations; + } + + @Override + public Integer getReconnectWarningId() { + return PumpWarningCodes.TBR_CANCELLED; + } + + @Override + public void execute() { + try { + if (checkAndWaitIfExistingTbrIsAboutToEnd()) { + return; + } + + enterTbrMenu(); + boolean increasingPercentage = inputTbrPercentage(); + verifyDisplayedTbrPercentage(increasingPercentage); + + if (percentage == 100) { + cancelTbrAndConfirmCancellationWarning(); + } else { + // switch to TBR_DURATION menu by pressing menu key + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + scripter.pressMenuKey(); + scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION); + + boolean increasingDuration = inputTbrDuration(); + verifyDisplayedTbrDuration(increasingDuration); + + // confirm TBR + scripter.pressCheckKey(); + scripter.waitForMenuToBeLeft(MenuType.TBR_DURATION); + } + } catch (CommandException e) { + if (scripter.getCurrentMenu().getType() == MenuType.WARNING_OR_ERROR) { + // The pump raises a TBR CANCELLED warning when a running TBR finishes while we're + // programming a new one (TBR remaining time was last displayed as 0:01, the pump + // rounds seconds up to full minutes). In that case confirm the alert since we know + // we caused it (in a way), but still fail the command so the usual cleanups of returning + // to main menu etc are performed, after which this command can simply be retried. + // Note that this situation should have been dealt with in + // #checkAndWaitIfExistingTbrIsAboutToEnd, but still occur if that method runs + // into a timeout or some other freaky thing happens, so we'll leave it here. + WarningOrErrorCode warningOrErrorCode = scripter.readWarningOrErrorCode(); + if (Objects.equals(warningOrErrorCode.warningCode, PumpWarningCodes.TBR_CANCELLED)) { + scripter.confirmAlert(PumpWarningCodes.TBR_CANCELLED); + } + } + throw e; + } + + result.success = true; + } + + /** + * When programming a new TBR while an existing TBR runs out, a TBR CANCELLED + * alert is raised (failing the command, requiring a reconnect and confirming alert + * and all). To avoid this, wait until the active TBR runs out if the active TBR + * is about to end. + * + * @return true if we waited till the TBR ended and cancellation was request so all work is done. + */ + private boolean checkAndWaitIfExistingTbrIsAboutToEnd() { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + long timeout = System.currentTimeMillis() + 65 * 1000; + PumpState state = scripter.readPumpStateInternal(); + if (state.tbrRemainingDuration == 1) { + while (state.tbrActive && System.currentTimeMillis() < timeout) { + log.debug("Waiting for existing TBR to run out to avoid alert while setting TBR"); + scripter.waitForScreenUpdate(); + state = scripter.readPumpStateInternal(); + } + // if we waited above and a cancellation was requested, we already completed the request + if (!state.tbrActive && percentage == 100) { + result.success = true; + return true; + } + } + return false; + } + + private void enterTbrMenu() { + scripter.verifyMenuIsDisplayed(MenuType.MAIN_MENU); + scripter.navigateToMenu(MenuType.TBR_MENU); + scripter.verifyMenuIsDisplayed(MenuType.TBR_MENU); + scripter.pressCheckKey(); + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + } + + private boolean inputTbrPercentage() { + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + long currentPercent = readDisplayedPercentage(); + log.debug("Current TBR %: " + currentPercent); + long percentageChange = percentage - currentPercent; + long percentageSteps = percentageChange / 10; + boolean increasePercentage = percentageSteps > 0; + log.debug("Pressing " + (increasePercentage ? "up" : "down") + " " + percentageSteps + " times"); + for (int i = 0; i < Math.abs(percentageSteps); i++) { + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + log.debug("Push #" + (i + 1) + "/" + Math.abs(percentageSteps)); + if (increasePercentage) scripter.pressUpKey(); + else scripter.pressDownKey(); + SystemClock.sleep(50); + } + return increasePercentage; + } + + private void verifyDisplayedTbrPercentage(boolean increasingPercentage) { + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + // wait up to 5s for any scrolling to finish + long displayedPercentage = readDisplayedPercentage(); + long timeout = System.currentTimeMillis() + 10 * 1000; + while (timeout > System.currentTimeMillis() + && ((increasingPercentage && displayedPercentage < percentage) + || (!increasingPercentage && displayedPercentage > percentage))) { + log.debug("Waiting for pump to process scrolling input for percentage, current: " + + displayedPercentage + ", desired: " + percentage + ", scrolling " + + (increasingPercentage ? "up" : "down")); + SystemClock.sleep(50); + displayedPercentage = readDisplayedPercentage(); + } + log.debug("Final displayed TBR percentage: " + displayedPercentage); + if (displayedPercentage != percentage) { + throw new CommandException("Failed to set TBR percentage, requested: " + + percentage + ", actual: " + displayedPercentage); + } + + // check again to ensure the displayed value hasn't change and scrolled past the desired + // value due to due scrolling taking extremely long + SystemClock.sleep(1000); + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + long refreshedDisplayedTbrPecentage = readDisplayedPercentage(); + if (displayedPercentage != refreshedDisplayedTbrPecentage) { + throw new CommandException("Failed to set TBR percentage: " + + "percentage changed after input stopped from " + + displayedPercentage + " -> " + refreshedDisplayedTbrPecentage); + } + } + + private boolean inputTbrDuration() { + scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION); + long durationSteps = calculateDurationSteps(); + boolean increaseDuration = durationSteps > 0; + log.debug("Pressing " + (increaseDuration ? "up" : "down") + " " + durationSteps + " times"); + for (int i = 0; i < Math.abs(durationSteps); i++) { + scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION); + log.debug("Push #" + (i + 1) + "/" + Math.abs(durationSteps)); + if (increaseDuration) scripter.pressUpKey(); + else scripter.pressDownKey(); + SystemClock.sleep(50); + } + return increaseDuration; + } + + private long calculateDurationSteps() { + long currentDuration = readDisplayedDuration(); + log.debug("Initial TBR duration: " + currentDuration); + + long difference = duration - currentDuration; + long durationSteps = difference / 15; + long durationAfterInitialSteps = currentDuration + (durationSteps * 15); + + if (durationAfterInitialSteps < duration) return durationSteps + 1; + else if (durationAfterInitialSteps > duration) return durationSteps - 1; + else return durationSteps; + } + + private void verifyDisplayedTbrDuration(boolean increasingPercentage) { + scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION); + + // wait up to 5s for any scrolling to finish + long displayedDuration = readDisplayedDuration(); + long timeout = System.currentTimeMillis() + 10 * 1000; + while (timeout > System.currentTimeMillis() + && ((increasingPercentage && displayedDuration < duration) + || (!increasingPercentage && displayedDuration > duration))) { + log.debug("Waiting for pump to process scrolling input for duration, current: " + + displayedDuration + ", desired: " + duration + + ", scrolling " + (increasingPercentage ? "up" : "down")); + SystemClock.sleep(50); + displayedDuration = readDisplayedDuration(); + } + + log.debug("Final displayed TBR duration: " + displayedDuration); + if (displayedDuration != duration) { + throw new CommandException("Failed to set TBR duration, requested: " + + duration + ", actual: " + displayedDuration); + } + + // check again to ensure the displayed value hasn't change and scrolled past the desired + // value due to due scrolling taking extremely long + SystemClock.sleep(1000); + scripter.verifyMenuIsDisplayed(MenuType.TBR_DURATION); + long refreshedDisplayedTbrDuration = readDisplayedDuration(); + if (displayedDuration != refreshedDisplayedTbrDuration) { + throw new CommandException("Failed to set TBR duration: " + + "duration changed after input stopped from " + + displayedDuration + " -> " + refreshedDisplayedTbrDuration); + } + } + + private void cancelTbrAndConfirmCancellationWarning() { + // confirm entered TBR + scripter.verifyMenuIsDisplayed(MenuType.TBR_SET); + scripter.pressCheckKey(); + + // A "TBR CANCELLED alert" is only raised by the pump when the remaining time is + // greater than 60s (displayed as 0:01, the pump goes from there to finished. + // We could read the remaining duration from MAIN_MENU, but by the time we're here, + // the pump could have moved from 0:02 to 0:01, so instead, check if a "TBR CANCELLED" alert + // is raised and if so dismiss it + scripter.confirmAlert(PumpWarningCodes.TBR_CANCELLED, 2000); + } + + private long readDisplayedDuration() { + MenuTime duration = scripter.readBlinkingValue(MenuTime.class, MenuAttribute.RUNTIME); + return duration.getHour() * 60 + duration.getMinute(); + } + + private long readDisplayedPercentage() { + return scripter.readBlinkingValue(Double.class, MenuAttribute.BASAL_RATE).longValue(); + } + + @Override + public boolean needsRunMode() { + return true; + } + + @Override + public String toString() { + return "SetTbrCommand{" + + "percentage=" + percentage + + ", duration=" + duration + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Bolus.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Bolus.java new file mode 100644 index 0000000000..a5d989c331 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Bolus.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history; + +import java.util.Date; + +public class Bolus extends HistoryRecord { + public final double amount; + public final boolean isValid; + + public Bolus(long timestamp, double amount, boolean isValid) { + super(timestamp); + this.amount = amount; + this.isValid = isValid; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Bolus bolus = (Bolus) o; + + if (timestamp != bolus.timestamp) return false; + if (isValid != bolus.isValid) return false; + return Math.abs(bolus.amount - amount) <= 0.01; + } + + @Override + public int hashCode() { + int result; + long temp; + result = (int) (timestamp ^ (timestamp >>> 32)); + temp = Double.doubleToLongBits(amount); + result = result + (isValid ? 1 : 0); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "Bolus{" + + "timestamp=" + timestamp + " (" + new Date(timestamp) + ")" + + ", amount=" + amount + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/HistoryRecord.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/HistoryRecord.java new file mode 100644 index 0000000000..57e173bb49 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/HistoryRecord.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history; + +public abstract class HistoryRecord { + public final long timestamp; + + protected HistoryRecord(long timestamp) { + this.timestamp = timestamp; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpAlert.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpAlert.java new file mode 100644 index 0000000000..413a9ca69b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpAlert.java @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history; + +import java.util.Date; + +public class PumpAlert extends HistoryRecord { + public final Integer warningCode; + public final Integer errorCode; + /** Error message, in the language configured on the pump. */ + public final String message; + + public PumpAlert(long timestamp, Integer warningCode, Integer errorCode, String message) { + super(timestamp); + this.warningCode = warningCode; + this.errorCode = errorCode; + this.message = message; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PumpAlert pumpAlert = (PumpAlert) o; + + if (timestamp != pumpAlert.timestamp) return false; + if (warningCode != null ? !warningCode.equals(pumpAlert.warningCode) : pumpAlert.warningCode != null) + return false; + if (errorCode != null ? !errorCode.equals(pumpAlert.errorCode) : pumpAlert.errorCode != null) + return false; + return message != null ? message.equals(pumpAlert.message) : pumpAlert.message == null; + } + + @Override + public int hashCode() { + int result = (int) (timestamp ^ (timestamp >>> 32)); + result = 31 * result + (warningCode != null ? warningCode.hashCode() : 0); + result = 31 * result + (errorCode != null ? errorCode.hashCode() : 0); + result = 31 * result + (message != null ? message.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "PumpAlert{" + + "timestamp=" + timestamp + "(" + new Date(timestamp) + ")" + + ", warningCode=" + warningCode + + ", errorCode=" + errorCode + + ", message='" + message + '\'' + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistory.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistory.java new file mode 100644 index 0000000000..ecc8a1a29f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistory.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** History data as read from the pump's My Data menu. + * Records are ordered from newest to oldest, so the first record is always the newest. */ +public class PumpHistory { + @NonNull + public List bolusHistory = new ArrayList<>(); + @NonNull + public List tbrHistory = new ArrayList<>(); + @NonNull + public List pumpAlertHistory = new LinkedList<>(); + @NonNull + public List tddHistory = new ArrayList<>(); + + public PumpHistory bolusHistory(List bolusHistory) { + this.bolusHistory = bolusHistory; + return this; + } + + public PumpHistory tbrHistory(List tbrHistory) { + this.tbrHistory = tbrHistory; + return this; + } + + public PumpHistory pumpErrorHistory(List pumpAlertHistory) { + this.pumpAlertHistory = pumpAlertHistory; + return this; + } + + public PumpHistory tddHistory(List tddHistory) { + this.tddHistory = tddHistory; + return this; + } + + @Override + public String toString() { + return "PumpHistory{" + + "bolusHistory=" + bolusHistory.size() + + ", tbrHistory=" + tbrHistory.size() + + ", pumpAlertHistory=" + pumpAlertHistory.size() + + ", tddHistory=" + tddHistory.size() + + '}'; + } +} 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 new file mode 100644 index 0000000000..e785c1d67c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/PumpHistoryRequest.java @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history; + +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 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; + public static final long FULL = 0; + + public long bolusHistory = SKIP; + public long tbrHistory = SKIP; + public long pumpErrorHistory = SKIP; + public long tddHistory = SKIP; + + public PumpHistoryRequest bolusHistory(long bolusHistory) { + this.bolusHistory = bolusHistory; + return this; + } + + public PumpHistoryRequest tbrHistory(long tbrHistory) { + this.tbrHistory = tbrHistory; + return this; + } + + public PumpHistoryRequest pumpErrorHistory(long pumpErrorHistory) { + this.pumpErrorHistory = pumpErrorHistory; + return this; + } + + public PumpHistoryRequest tddHistory(long tddHistory) { + this.tddHistory = tddHistory; + return this; + } + + @Override + public String toString() { + return "PumpHistoryRequest{" + + "bolusHistory=" + bolusHistory + (bolusHistory > 0 ? ("(" + new Date(bolusHistory) + ")") : "") + + ", tbrHistory=" + tbrHistory + (tbrHistory > 0 ? ("(" + new Date(tbrHistory) + ")") : "") + + ", pumpAlertHistory=" + pumpErrorHistory + (pumpErrorHistory > 0 ? ("(" + new Date(pumpErrorHistory) + ")") : "") + + ", tddHistory=" + tddHistory + (tddHistory > 0 ? ("(" + new Date(tddHistory) + ")") : "") + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Tbr.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Tbr.java new file mode 100644 index 0000000000..41ec245e89 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Tbr.java @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history; + +import java.util.Date; + +public class Tbr extends HistoryRecord { + /** Duration in minutes */ + public final int duration; + public final int percent; + + public Tbr(long timestamp, int duration, int percent) { + super(timestamp); + this.duration = duration; + this.percent = percent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Tbr tbr = (Tbr) o; + + if (timestamp != tbr.timestamp) return false; + if (duration != tbr.duration) return false; + return percent == tbr.percent; + } + + @Override + public int hashCode() { + int result = (int) (timestamp ^ (timestamp >>> 32)); + result = 31 * result + duration; + result = 31 * result + percent; + return result; + } + + @Override + public String toString() { + return "Tbr{" + + "timestamp=" + timestamp + "(" + new Date(timestamp) + ")" + + ", duration=" + duration + + ", percent=" + percent + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Tdd.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Tdd.java new file mode 100644 index 0000000000..5f799f229e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpCombo/ruffyscripter/history/Tdd.java @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history; + +import java.util.Date; + +/** Total daily dosage; amount of insulin delivered over a full day. */ +public class Tdd extends HistoryRecord { + public final double total; + + public Tdd(long timestamp, double total) { + super(timestamp); + this.total = total; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Tdd tdd = (Tdd) o; + + if (timestamp != tdd.timestamp) return false; + return tdd.total != total; + } + + @Override + public int hashCode() { + int result; + long temp; + result = (int) (timestamp ^ (timestamp >>> 32)); + temp = Double.doubleToLongBits(total); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "Tdd{" + + "timestamp=" + timestamp + "(" + new Date(timestamp) + ")" + + ", total=" + total + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Wear/WearPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/Wear/WearPlugin.java index 3c1e827fe6..5f11fc5fda 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Wear/WearPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Wear/WearPlugin.java @@ -31,8 +31,8 @@ import info.nightscout.utils.SP; public class WearPlugin implements PluginBase { - private static boolean fragmentEnabled = true; - private boolean fragmentVisible = true; + private static boolean fragmentEnabled = false; + private boolean fragmentVisible = false; private static WatchUpdaterService watchUS; private final Context ctx; diff --git a/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java b/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java index a254855cab..748ada61f6 100644 --- a/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java +++ b/app/src/main/java/info/nightscout/androidaps/queue/CommandQueue.java @@ -2,6 +2,7 @@ package info.nightscout.androidaps.queue; import android.content.Context; import android.content.Intent; +import android.os.SystemClock; import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.text.Spanned; @@ -81,7 +82,7 @@ public class CommandQueue { return new PumpEnactResult().success(false).enacted(false).comment(MainApp.sResources.getString(R.string.executingrightnow)); } - public boolean isRunning(Command.CommandType type) { + private boolean isRunning(Command.CommandType type) { if (performing != null && performing.commandType == type) return true; return false; @@ -293,6 +294,21 @@ public class CommandQueue { return false; } + // Check that there is a valid profileSwitch NOW + if (MainApp.getConfigBuilder().getProfileSwitchFromHistory(System.currentTimeMillis())==null) { + // wait for DatabaseHelper.scheduleProfiSwitch() to do the profile switch // TODO clean this crap up + SystemClock.sleep(5000); + if (MainApp.getConfigBuilder().getProfileSwitchFromHistory(System.currentTimeMillis())==null) { + Notification noProfileSwitchNotif = new Notification(Notification.PROFILE_SWITCH_MISSING, MainApp.gs(R.string.profileswitch_ismissing), Notification.NORMAL); + MainApp.bus().post(new EventNewNotification(noProfileSwitchNotif)); + if (callback != null) { + PumpEnactResult result = new PumpEnactResult().success(false).enacted(false).comment("Refuse to send profile to pump! No ProfileSwitch!"); + callback.result(result).run(); + } + return false; + } + } + if (!MainApp.isEngineeringModeOrRelease()) { Notification notification = new Notification(Notification.NOT_ENG_MODE_OR_RELEASE, MainApp.sResources.getString(R.string.not_eng_mode_or_release), Notification.URGENT); MainApp.bus().post(new EventNewNotification(notification)); @@ -305,8 +321,8 @@ public class CommandQueue { Profile.BasalValue[] basalValues = profile.getBasalValues(); PumpInterface pump = ConfigBuilderPlugin.getActivePump(); - for (int index = 0; index < basalValues.length; index++) { - if (basalValues[index].value < pump.getPumpDescription().basalMinimumRate) { + for (Profile.BasalValue basalValue : basalValues) { + if (basalValue.value < pump.getPumpDescription().basalMinimumRate) { Notification notification = new Notification(Notification.BASAL_VALUE_BELOW_MINIMUM, MainApp.sResources.getString(R.string.basalvaluebelowminimum), Notification.URGENT); MainApp.bus().post(new EventNewNotification(notification)); if (callback != null) diff --git a/app/src/main/java/info/nightscout/androidaps/queue/QueueThread.java b/app/src/main/java/info/nightscout/androidaps/queue/QueueThread.java index f241ab74cf..625d50a873 100644 --- a/app/src/main/java/info/nightscout/androidaps/queue/QueueThread.java +++ b/app/src/main/java/info/nightscout/androidaps/queue/QueueThread.java @@ -28,9 +28,8 @@ import info.nightscout.utils.SP; public class QueueThread extends Thread { private static Logger log = LoggerFactory.getLogger(QueueThread.class); - CommandQueue queue; + private CommandQueue queue; - private long connectionStartTime = 0; private long lastCommandTime = 0; private boolean connectLogged = false; @@ -48,7 +47,7 @@ public class QueueThread extends Thread { public final void run() { mWakeLock.acquire(); MainApp.bus().post(new EventQueueChanged()); - connectionStartTime = lastCommandTime = System.currentTimeMillis(); + long connectionStartTime = lastCommandTime = System.currentTimeMillis(); try { while (true) { diff --git a/app/src/main/java/info/nightscout/utils/DateUtil.java b/app/src/main/java/info/nightscout/utils/DateUtil.java index 86965621d2..102720ef1b 100644 --- a/app/src/main/java/info/nightscout/utils/DateUtil.java +++ b/app/src/main/java/info/nightscout/utils/DateUtil.java @@ -124,7 +124,12 @@ public class DateUtil { public static String minAgo(long time) { int mins = (int) ((System.currentTimeMillis() - time) / 1000 / 60); - return String.format(MainApp.sResources.getString(R.string.minago), mins); + return MainApp.gs(R.string.minago, mins); + } + + public static String hourAgo(long time) { + double hours = (System.currentTimeMillis() - time) / 1000d / 60 / 60; + return MainApp.gs(R.string.hoursago, hours); } private static LongSparseArray timeStrings = new LongSparseArray<>(); diff --git a/app/src/main/java/info/nightscout/utils/LocalAlertUtils.java b/app/src/main/java/info/nightscout/utils/LocalAlertUtils.java index 66cabd9db6..58814ceddb 100644 --- a/app/src/main/java/info/nightscout/utils/LocalAlertUtils.java +++ b/app/src/main/java/info/nightscout/utils/LocalAlertUtils.java @@ -15,6 +15,8 @@ import info.nightscout.androidaps.interfaces.PumpInterface; import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; import info.nightscout.androidaps.plugins.Overview.events.EventNewNotification; import info.nightscout.androidaps.plugins.Overview.notifications.Notification; +import info.nightscout.androidaps.receivers.KeepAliveReceiver; +import info.nightscout.utils.NSUpload; /** * Created by adrian on 17/12/17. diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/Menu.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/Menu.java new file mode 100644 index 0000000000..cd3b7be770 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/Menu.java @@ -0,0 +1,142 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import org.monkey.d.ruffy.ruffy.driver.display.menu.BolusType; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuBlink; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuDate; +import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Created by fishermen21 on 20.05.17. + */ + +public class Menu implements Parcelable{ + private MenuType type; + private Map attributes = new HashMap<>(); + + public Menu(MenuType type) + { + this.type = type; + } + + public Menu(Parcel in) { + this.type = MenuType.valueOf(in.readString()); + while(in.dataAvail()>0) { + try { + String attr = in.readString(); + String clas = in.readString(); + String value = in.readString(); + + if(attr!=null && clas!=null && value!=null) { + MenuAttribute a = MenuAttribute.valueOf(attr); + Object o = null; + if (Integer.class.toString().equals(clas)) { + o = new Integer(value); + } else if (Double.class.toString().equals(clas)) { + o = new Double(value); + } else if (Boolean.class.toString().equals(clas)) { + o = new Boolean(value); + } else if (MenuDate.class.toString().equals(clas)) { + o = new MenuDate(value); + } else if (MenuTime.class.toString().equals(clas)) { + o = new MenuTime(value); + } else if (MenuBlink.class.toString().equals(clas)) { + o = new MenuBlink(); + } else if (BolusType.class.toString().equals(clas)) { + o = BolusType.valueOf(value); + } else if (String.class.toString().equals(clas)) { + o = new String(value); + } + + if (o != null) { + attributes.put(a, o); + } else { + Log.e("MenuIn", "failed to parse: " + attr + " / " + clas + " / " + value); + } + } + }catch(Exception e) + { + Log.e("MenuIn","Exception in read",e); + } + + } + } + + public void setAttribute(MenuAttribute key, Object value) + { + attributes.put(key,value); + } + + public List attributes() + { + return new LinkedList(attributes.keySet()); + } + + public Object getAttribute(MenuAttribute key) + { + return attributes.get(key); + } + + public MenuType getType() { + return type; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type.toString()); + for(MenuAttribute a : attributes.keySet()) + { + try + { + + String atr = a.toString(); + Object o = attributes.get(a); + String clas = o.getClass().toString(); + String v = o.toString(); + if(atr != null && o != null && v != null) { + dest.writeString(atr); + dest.writeString(clas); + dest.writeString(v); + } + else + { + Log.e("Menu","null in write :/"); + } + }catch(Exception e) + { + Log.v("MenuOut","error in write",e); + } + } + } + public static final Parcelable.Creator

CREATOR = new + Parcelable.Creator() { + public Menu createFromParcel(Parcel in) { + return new Menu(in); + } + + public Menu[] newArray(int size) { + return new Menu[size]; + } + }; + + @Override + public String toString() { + return "Menu{" + + "type=" + type + + ", attributes=" + attributes + + '}'; + } +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuAttribute.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuAttribute.java new file mode 100644 index 0000000000..0404c89011 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuAttribute.java @@ -0,0 +1,34 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public enum MenuAttribute { + RUNTIME,//runtime of current operation, remaining time on main menu + BOLUS,//double units + BOLUS_REMAINING,//double units remain from current bolus + TBR,//double 0-500% + BASAL_RATE,//double units/h + BASAL_SELECTED,//int selected basal profile + BATTERY_STATE,//int, like insulin state + INSULIN_STATE,//int insulin warning 0 == no warning, 1== low, 2 == empty + LOCK_STATE,//int keylock state 0==no lock, 1==unlocked, 2==locked + MULTIWAVE_BOLUS,//double immediate bolus on multiwave + BOLUS_TYPE,//BolusType, only history uses MULTIWAVE + TIME,//time MenuTime + REMAINING_INSULIN,//double units + DATE,//date MenuDate + CURRENT_RECORD,//int current record + TOTAL_RECORD, //int total num record + ERROR, //int errorcode + WARNING, //int errorcode + MESSAGE, //string errormessage + DAILY_TOTAL, //double units + BASAL_TOTAL, //double total basal + BASAL_START, //time MenuTime the basalrate starts + BASAL_END, // time MenuTime the basalrate ends + DEBUG_TIMING, //double with timing infos + WARANTY, //boolean true if out of waranty + ERROR_OR_WARNING, // set if menu in blink during error/warning +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuType.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuType.java new file mode 100644 index 0000000000..4d00ea9155 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/MenuType.java @@ -0,0 +1,42 @@ +package org.monkey.d.ruffy.ruffy.driver.display; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public enum MenuType { + MAIN_MENU, + STOP_MENU, + BOLUS_MENU, + BOLUS_ENTER, + EXTENDED_BOLUS_MENU, + BOLUS_DURATION, + MULTIWAVE_BOLUS_MENU, + IMMEDIATE_BOLUS, + TBR_MENU, + MY_DATA_MENU, + BASAL_MENU, + BASAL_1_MENU, + BASAL_2_MENU, + BASAL_3_MENU, + BASAL_4_MENU, + BASAL_5_MENU, + DATE_AND_TIME_MENU, + ALARM_MENU, + MENU_SETTINGS_MENU, + BLUETOOTH_MENU, + THERAPY_MENU, + PUMP_MENU, + QUICK_INFO, + BOLUS_DATA, + DAILY_DATA, + TBR_DATA, + ERROR_DATA, + TBR_SET, + TBR_DURATION, + STOP, + START_MENU, + BASAL_TOTAL, + BASAL_SET, + WARNING_OR_ERROR, +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/BolusType.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/BolusType.java new file mode 100644 index 0000000000..82572acc93 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/BolusType.java @@ -0,0 +1,13 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public enum BolusType{ + NORMAL, + EXTENDED, + MULTIWAVE, + MULTIWAVE_BOLUS, + MULTIWAVE_EXTENDED, +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuBlink.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuBlink.java new file mode 100644 index 0000000000..18c0374dcb --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuBlink.java @@ -0,0 +1,12 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public class MenuBlink { + @Override + public String toString() { + return "BLINK"; + } +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuDate.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuDate.java new file mode 100644 index 0000000000..05cc741413 --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuDate.java @@ -0,0 +1,35 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 24.05.17. + */ + +public class MenuDate { + private final int day; + private final int month; + + + public MenuDate(int day, int month) { + this.day = day; + this.month = month; + } + + public MenuDate(String value) { + String[] p = value.split("\\."); + day = Integer.parseInt(p[0]); + month = Integer.parseInt(p[1]); + } + + public int getDay() { + return day; + } + + public int getMonth() { + return month; + } + + @Override + public String toString() { + return day+"."+String.format("%02d",month)+"."; + } +} diff --git a/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuTime.java b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuTime.java new file mode 100644 index 0000000000..147aafc8eb --- /dev/null +++ b/app/src/main/java/org/monkey/d/ruffy/ruffy/driver/display/menu/MenuTime.java @@ -0,0 +1,36 @@ +package org.monkey.d.ruffy.ruffy.driver.display.menu; + +/** + * Created by fishermen21 on 22.05.17. + */ + +public class MenuTime { + + private final int hour; + private final int minute; + + public MenuTime(int hour, int minute) + { + this.hour = hour; + this.minute = minute; + } + + public MenuTime(String value) { + String[] p = value.split(":"); + hour = Integer.parseInt(p[0]); + minute = Integer.parseInt(p[1]); + } + + public int getHour() { + return hour; + } + + public int getMinute() { + return minute; + } + + @Override + public String toString() { + return hour+":"+String.format("%02d",minute); + } +} diff --git a/app/src/main/res/layout/combo_alert_history_fragment.xml b/app/src/main/res/layout/combo_alert_history_fragment.xml new file mode 100644 index 0000000000..f1faa9bb86 --- /dev/null +++ b/app/src/main/res/layout/combo_alert_history_fragment.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/combo_tdd_history_fragment.xml b/app/src/main/res/layout/combo_tdd_history_fragment.xml new file mode 100644 index 0000000000..04335526ba --- /dev/null +++ b/app/src/main/res/layout/combo_tdd_history_fragment.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/combopump_fragment.xml b/app/src/main/res/layout/combopump_fragment.xml new file mode 100644 index 0000000000..67b135b677 --- /dev/null +++ b/app/src/main/res/layout/combopump_fragment.xml @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/overview_bolusprogress_dialog.xml b/app/src/main/res/layout/overview_bolusprogress_dialog.xml index 2caa547c1e..85fe401052 100644 --- a/app/src/main/res/layout/overview_bolusprogress_dialog.xml +++ b/app/src/main/res/layout/overview_bolusprogress_dialog.xml @@ -11,6 +11,8 @@ android:id="@+id/overview_bolusprogress_status" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingLeft="10dp" + android:paddingRight="10dp" android:layout_gravity="center_horizontal" />