diff --git a/app/build.gradle b/app/build.gradle index 3f7dac00a9..00c78fe077 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ android { targetSdkVersion 23 multiDexEnabled true versionCode 1500 - version "1.59-dev" + version "1.60-dev" buildConfigField "String", "VERSION", '"' + version + '"' buildConfigField "String", "BUILDVERSION", generateGitBuild() @@ -238,6 +238,7 @@ dependencies { testCompile "org.powermock:powermock-module-junit4:${powermockVersion}" testCompile "joda-time:joda-time:2.9.4.2" testCompile "com.google.truth:truth:0.39" + testCompile "org.skyscreamer:jsonassert:1.5.0" androidTestCompile "org.mockito:mockito-core:2.7.22" androidTestCompile "com.google.dexmaker:dexmaker:${dexmakerVersion}" 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/assets/logback.xml b/app/src/main/assets/logback.xml index 97e09b3e4d..d6facdb7dd 100644 --- a/app/src/main/assets/logback.xml +++ b/app/src/main/assets/logback.xml @@ -1,6 +1,6 @@ - + ${EXT_FILES_DIR}/AndroidAPS.log 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 d96f432415..0729e9d4b1 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 ba6908a2dd..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; @@ -400,10 +404,16 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe builder.setIcon(R.mipmap.blueowl); String message = "Build: " + BuildConfig.BUILDVERSION + "\n"; message += MainApp.sResources.getString(R.string.configbuilder_nightscoutversion_label) + " " + ConfigBuilderPlugin.nightscoutVersionName; - builder.setMessage(message); + if (MainApp.engineeringMode) + message += "\n" + MainApp.gs(R.string.engineering_mode_enabled); + 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 2069ea557b..5087fefefa 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -20,8 +20,10 @@ import net.danlew.android.joda.JodaTimeAndroid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.util.ArrayList; +import ch.qos.logback.classic.LoggerContext; import info.nightscout.androidaps.Services.Intents; import info.nightscout.androidaps.db.DatabaseHelper; import info.nightscout.androidaps.interfaces.InsulinInterface; @@ -46,10 +48,13 @@ import info.nightscout.androidaps.plugins.OpenAPSAMA.OpenAPSAMAPlugin; import info.nightscout.androidaps.plugins.OpenAPSMA.OpenAPSMAPlugin; import info.nightscout.androidaps.plugins.OpenAPSSMB.OpenAPSSMBPlugin; import info.nightscout.androidaps.plugins.Overview.OverviewPlugin; +import info.nightscout.androidaps.plugins.Overview.events.EventNewNotification; +import info.nightscout.androidaps.plugins.Overview.notifications.Notification; import info.nightscout.androidaps.plugins.Persistentnotification.PersistentNotificationPlugin; 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; @@ -74,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; @@ -95,6 +101,9 @@ public class MainApp extends Application { private static AckAlarmReceiver ackAlarmReciever = new AckAlarmReceiver(); private LocalBroadcastManager lbm; + public static boolean devBranch; + public static boolean engineeringMode; + @Override public void onCreate() { super.onCreate(); @@ -116,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(); @@ -134,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) 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()); @@ -195,6 +211,10 @@ public class MainApp extends Application { } }).start(); + 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)); + } } private void registerLocalBroadcastReceiver() { @@ -361,6 +381,15 @@ public class MainApp extends Application { return null; } + public static boolean isEngineeringModeOrRelease() { + return engineeringMode || !devBranch; + } + + private String getLogDirectory() { + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + return lc.getProperty("EXT_FILES_DIR"); + } + @Override public void onTerminate() { super.onTerminate(); 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/Services/DataService.java b/app/src/main/java/info/nightscout/androidaps/Services/DataService.java index 14465ccdc9..07ca0fc049 100644 --- a/app/src/main/java/info/nightscout/androidaps/Services/DataService.java +++ b/app/src/main/java/info/nightscout/androidaps/Services/DataService.java @@ -96,7 +96,7 @@ public class DataService extends IntentService { dexcomG5Enabled = true; } - boolean isNSProfile = ConfigBuilderPlugin.getActiveProfileInterface().getClass().equals(NSProfilePlugin.class); + boolean isNSProfile = MainApp.getConfigBuilder().getActiveProfileInterface().getClass().equals(NSProfilePlugin.class); boolean acceptNSData = !SP.getBoolean(R.string.key_ns_upload_only, false); Bundle bundles = intent.getExtras(); @@ -192,7 +192,7 @@ public class DataService extends IntentService { bgReading.date = bundle.getLong(Intents.EXTRA_TIMESTAMP); bgReading.raw = bundle.getDouble(Intents.EXTRA_RAW); - MainApp.getDbHelper().createIfNotExists(bgReading, "XDRIP"); + MainApp.getDbHelper().createIfNotExists(bgReading, "XDRIP", xDripEnabled); } private void handleNewDataFromGlimp(Intent intent) { @@ -206,7 +206,7 @@ public class DataService extends IntentService { bgReading.date = bundle.getLong("myTimestamp"); bgReading.raw = 0; - MainApp.getDbHelper().createIfNotExists(bgReading, "GLIMP"); + MainApp.getDbHelper().createIfNotExists(bgReading, "GLIMP", glimpEnabled); } private void handleNewDataFromDexcomG5(Intent intent) { @@ -229,7 +229,7 @@ public class DataService extends IntentService { bgReading.direction = json.getString("m_trend"); bgReading.date = json.getLong("m_time") * 1000L; bgReading.raw = 0; - boolean isNew = MainApp.getDbHelper().createIfNotExists(bgReading, "DexcomG5"); + boolean isNew = MainApp.getDbHelper().createIfNotExists(bgReading, "DexcomG5", dexcomG5Enabled); if (isNew && SP.getBoolean(R.string.key_dexcomg5_nsupload, false)) { NSUpload.uploadBg(bgReading); } @@ -268,7 +268,7 @@ public class DataService extends IntentService { bgReading.date = json_object.getLong("date"); bgReading.raw = json_object.getDouble("sgv"); - MainApp.getDbHelper().createIfNotExists(bgReading, "MM640g"); + MainApp.getDbHelper().createIfNotExists(bgReading, "MM640g", mm640gEnabled); break; default: log.debug("Unknown entries type: " + type); @@ -368,9 +368,6 @@ public class DataService extends IntentService { ProfileStore profileStore = new ProfileStore(new JSONObject(profile)); NSProfilePlugin.getPlugin().storeNewProfile(profileStore); MainApp.bus().post(new EventNSProfileUpdateGUI()); - // if there are no profile switches this should lead to profile update - if (MainApp.getConfigBuilder().getProfileSwitchesFromHistory().size() == 0) - MainApp.bus().post(new EventNewBasalProfile()); if (Config.logIncommingData) log.debug("Received profileStore: " + activeProfile + " " + profile); } catch (JSONException e) { @@ -428,7 +425,7 @@ public class DataService extends IntentService { JSONObject sgvJson = new JSONObject(sgvstring); NSSgv nsSgv = new NSSgv(sgvJson); BgReading bgReading = new BgReading(nsSgv); - MainApp.getDbHelper().createIfNotExists(bgReading, "NS"); + MainApp.getDbHelper().createIfNotExists(bgReading, "NS", nsClientEnabled); } if (bundles.containsKey("sgvs")) { @@ -438,7 +435,7 @@ public class DataService extends IntentService { JSONObject sgvJson = jsonArray.getJSONObject(i); NSSgv nsSgv = new NSSgv(sgvJson); BgReading bgReading = new BgReading(nsSgv); - MainApp.getDbHelper().createIfNotExists(bgReading, "NS"); + MainApp.getDbHelper().createIfNotExists(bgReading, "NS", nsClientEnabled); } } } catch (Exception e) { 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/Profile.java b/app/src/main/java/info/nightscout/androidaps/data/Profile.java index 5c6d4c9629..e966c4d11d 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/Profile.java +++ b/app/src/main/java/info/nightscout/androidaps/data/Profile.java @@ -2,8 +2,6 @@ package info.nightscout.androidaps.data; import android.support.v4.util.LongSparseArray; - - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -19,41 +17,43 @@ import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; 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.notifications.Notification; import info.nightscout.utils.DateUtil; import info.nightscout.utils.DecimalFormatter; import info.nightscout.utils.FabricPrivacy; -import info.nightscout.utils.ToastUtils; public class Profile { private static Logger log = LoggerFactory.getLogger(Profile.class); private JSONObject json; - private String units = null; - private double dia = Constants.defaultDIA; - private TimeZone timeZone = TimeZone.getDefault(); + private String units; + private double dia; + private TimeZone timeZone; private JSONArray isf; - private LongSparseArray isf_v = null; // oldest at index 0 + private LongSparseArray isf_v; // oldest at index 0 private JSONArray ic; - private LongSparseArray ic_v = null; // oldest at index 0 + private LongSparseArray ic_v; // oldest at index 0 private JSONArray basal; - private LongSparseArray basal_v = null; // oldest at index 0 + private LongSparseArray basal_v; // oldest at index 0 private JSONArray targetLow; - private LongSparseArray targetLow_v = null; // oldest at index 0 + private LongSparseArray targetLow_v; // oldest at index 0 private JSONArray targetHigh; - private LongSparseArray targetHigh_v = null; // oldest at index 0 + private LongSparseArray targetHigh_v; // oldest at index 0 - private int percentage = 100; - private int timeshift = 0; + private int percentage; + private int timeshift; - private boolean isValid = true; - private boolean isValidated = false; + protected boolean isValid; + protected boolean isValidated; + // Default constructor for tests + protected Profile() { + } + + // Constructor from profileStore JSON public Profile(JSONObject json, String units) { - this(json, 100, 0); + init(json, 100, 0); if (this.units == null) { if (units != null) this.units = units; @@ -65,6 +65,22 @@ public class Profile { } public Profile(JSONObject json, int percentage, int timeshift) { + init(json, percentage, timeshift); + } + + protected void init(JSONObject json, int percentage, int timeshift) { + units = null; + dia = Constants.defaultDIA; + timeZone = TimeZone.getDefault(); + isf_v = null; + ic_v = null; + basal_v = null; + targetLow_v = null; + targetHigh_v = null; + + isValid = true; + isValidated = false; + this.percentage = percentage; this.timeshift = timeshift; this.json = json; @@ -78,53 +94,12 @@ public class Profile { if (json.has("timezone")) timeZone = TimeZone.getTimeZone(json.getString("timezone")); isf = json.getJSONArray("sens"); - if (getIsf(0) == null) { - int defaultISF = units.equals(Constants.MGDL) ? 400 : 20; - isf = new JSONArray("[{\"time\":\"00:00\",\"value\":\"" + defaultISF + "\",\"timeAsSeconds\":\"0\"}]"); - Notification noisf = new Notification(Notification.ISF_MISSING, MainApp.sResources.getString(R.string.isfmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(noisf)); - } else { - MainApp.bus().post(new EventDismissNotification(Notification.ISF_MISSING)); - } ic = json.getJSONArray("carbratio"); - if (getIc(0) == null) { - int defaultIC = 25; - ic = new JSONArray("[{\"time\":\"00:00\",\"value\":\"" + defaultIC + "\",\"timeAsSeconds\":\"0\"}]"); - Notification noic = new Notification(Notification.IC_MISSING, MainApp.sResources.getString(R.string.icmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(noic)); - } else { - MainApp.bus().post(new EventDismissNotification(Notification.IC_MISSING)); - } basal = json.getJSONArray("basal"); - if (getBasal(0) == null) { - double defaultBasal = 0.1d; - basal = new JSONArray("[{\"time\":\"00:00\",\"value\":\"" + defaultBasal + "\",\"timeAsSeconds\":\"0\"}]"); - Notification nobasal = new Notification(Notification.BASAL_MISSING, MainApp.sResources.getString(R.string.basalmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(nobasal)); - } else { - MainApp.bus().post(new EventDismissNotification(Notification.BASAL_MISSING)); - } targetLow = json.getJSONArray("target_low"); - if (getTargetLow(0) == null) { - double defaultLow = units.equals(Constants.MGDL) ? 120 : 6; - targetLow = new JSONArray("[{\"time\":\"00:00\",\"value\":\"" + defaultLow + "\",\"timeAsSeconds\":\"0\"}]"); - Notification notarget = new Notification(Notification.TARGET_MISSING, MainApp.sResources.getString(R.string.targetmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(notarget)); - } else { - MainApp.bus().post(new EventDismissNotification(Notification.TARGET_MISSING)); - } targetHigh = json.getJSONArray("target_high"); - if (getTargetHigh(0) == null) { - double defaultHigh = units.equals(Constants.MGDL) ? 160 : 8; - targetHigh = new JSONArray("[{\"time\":\"00:00\",\"value\":\"" + defaultHigh + "\",\"timeAsSeconds\":\"0\"}]"); - Notification notarget = new Notification(Notification.TARGET_MISSING, MainApp.sResources.getString(R.string.targetmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(notarget)); - } else { - MainApp.bus().post(new EventDismissNotification(Notification.TARGET_MISSING)); - } } catch (JSONException e) { log.error("Unhandled exception", e); - ToastUtils.showToastInUiThread(MainApp.instance().getApplicationContext(), MainApp.gs(R.string.invalidprofile)); isValid = false; isValidated = true; } @@ -133,7 +108,7 @@ public class Profile { public String log() { String ret = "\n"; for (Integer hour = 0; hour < 24; hour++) { - double value = getBasal((Integer) (hour * 60 * 60)); + double value = getBasalTimeFromMidnight((Integer) (hour * 60 * 60)); ret += "NS basal value for " + hour + ":00 is " + value + "\n"; } ret += "NS units: " + getUnits(); @@ -155,6 +130,10 @@ public class Profile { } // mmol or mg/dl + public void setUnits(String units) { + this.units = units; + } + public String getUnits() { return units; } @@ -164,6 +143,11 @@ public class Profile { } private LongSparseArray convertToSparseArray(JSONArray array) { + if (array == null) { + isValid = false; + return new LongSparseArray<>(); + } + double multiplier = getMultiplier(array); LongSparseArray sparse = new LongSparseArray<>(); @@ -235,7 +219,7 @@ public class Profile { for (int i = 0; i < basal_v.size(); i++) { if (basal_v.valueAt(i) < description.basalMinimumRate) { basal_v.setValueAt(i, description.basalMinimumRate); - MainApp.bus().post(new EventNewNotification(new Notification(Notification.MINIMAL_BASAL_VALUE_REPLACED, String.format(MainApp.gs(R.string.minimalbasalvaluereplaced), from), Notification.NORMAL))); + sendBelowMinimumNotification(from); } } } else { @@ -249,6 +233,10 @@ public class Profile { return isValid; } + protected void sendBelowMinimumNotification(String from) { + MainApp.bus().post(new EventNewNotification(new Notification(Notification.MINIMAL_BASAL_VALUE_REPLACED, String.format(MainApp.gs(R.string.minimalbasalvaluereplaced), from), Notification.NORMAL))); + } + private void validate(LongSparseArray array) { if (array.size() == 0) { isValid = false; @@ -262,6 +250,7 @@ public class Profile { } } + /* private Double getValueToTime(JSONArray array, Integer timeAsSeconds) { Double lastValue = null; @@ -281,6 +270,7 @@ public class Profile { } return lastValue; } + */ Integer getShitfTimeSecs(Integer originalTime) { Integer shiftedTime = originalTime + timeshift * 60 * 60; @@ -322,7 +312,7 @@ public class Profile { return multiplier; } - private Double getValueToTime(LongSparseArray array, Integer timeAsSeconds) { + private double getValueToTime(LongSparseArray array, Integer timeAsSeconds) { Double lastValue = null; for (Integer index = 0; index < array.size(); index++) { @@ -337,7 +327,7 @@ public class Profile { return lastValue; } - private String format_HH_MM(Integer timeAsSeconds) { + protected String format_HH_MM(Integer timeAsSeconds) { String time; int hour = timeAsSeconds / 60 / 60; int minutes = (timeAsSeconds - hour * 60 * 60) / 60; @@ -364,51 +354,55 @@ public class Profile { return retValue; } - public Double getIsf() { - return getIsf(secondsFromMidnight(System.currentTimeMillis())); + public double getIsf() { + return getIsfTimeFromMidnight(secondsFromMidnight(System.currentTimeMillis())); } - public Double getIsf(long time) { - return getIsf(secondsFromMidnight(time)); + public double getIsf(long time) { + return getIsfTimeFromMidnight(secondsFromMidnight(time)); } - public Double getIsf(Integer timeAsSeconds) { + double getIsfTimeFromMidnight(int timeAsSeconds) { if (isf_v == null) isf_v = convertToSparseArray(isf); return getValueToTime(isf_v, timeAsSeconds); } public String getIsfList() { + if (isf_v == null) + isf_v = convertToSparseArray(isf); return getValuesList(isf_v, null, new DecimalFormat("0.0"), getUnits() + "/U"); } - public Double getIc() { - return getIc(secondsFromMidnight(System.currentTimeMillis())); + public double getIc() { + return getIcTimeFromMidnight(secondsFromMidnight(System.currentTimeMillis())); } - public Double getIc(long time) { - return getIc(secondsFromMidnight(time)); + public double getIc(long time) { + return getIcTimeFromMidnight(secondsFromMidnight(time)); } - public Double getIc(Integer timeAsSeconds) { + public double getIcTimeFromMidnight(int timeAsSeconds) { if (ic_v == null) ic_v = convertToSparseArray(ic); return getValueToTime(ic_v, timeAsSeconds); } public String getIcList() { - return getValuesList(ic_v, null, new DecimalFormat("0.0"), " g/U"); + if (ic_v == null) + ic_v = convertToSparseArray(ic); + return getValuesList(ic_v, null, new DecimalFormat("0.0"), "g/U"); } - public Double getBasal() { - return getBasal(secondsFromMidnight(System.currentTimeMillis())); + public double getBasal() { + return getBasalTimeFromMidnight(secondsFromMidnight(System.currentTimeMillis())); } - public Double getBasal(long time) { - return getBasal(secondsFromMidnight(time)); + public double getBasal(long time) { + return getBasalTimeFromMidnight(secondsFromMidnight(time)); } - public synchronized Double getBasal(Integer timeAsSeconds) { + public synchronized double getBasalTimeFromMidnight(int timeAsSeconds) { if (basal_v == null) { basal_v = convertToSparseArray(basal); } @@ -418,17 +412,17 @@ public class Profile { public String getBasalList() { if (basal_v == null) basal_v = convertToSparseArray(basal); - return getValuesList(basal_v, null, new DecimalFormat("0.00"), "U"); + return getValuesList(basal_v, null, new DecimalFormat("0.00"), "U/h"); } public class BasalValue { - public BasalValue(Integer timeAsSeconds, Double value) { + public BasalValue(int timeAsSeconds, double value) { this.timeAsSeconds = timeAsSeconds; this.value = value; } - public Integer timeAsSeconds; - public Double value; + public int timeAsSeconds; + public double value; } public synchronized BasalValue[] getBasalValues() { @@ -438,7 +432,7 @@ public class Profile { for (Integer index = 0; index < basal_v.size(); index++) { Integer tas = (int) basal_v.keyAt(index); - Double value = basal_v.valueAt(index); + double value = basal_v.valueAt(index); ret[index] = new BasalValue(tas, value); } return ret; @@ -448,52 +442,56 @@ public class Profile { return getTarget(secondsFromMidnight(System.currentTimeMillis())); } - private double getTarget(Integer time) { - return (getTargetLow(time) + getTargetHigh(time))/2; + protected double getTarget(int timeAsSeconds) { + return (getTargetLowTimeFromMidnight(timeAsSeconds) + getTargetHighTimeFromMidnight(timeAsSeconds))/2; } - public Double getTargetLow() { - return getTargetLow(secondsFromMidnight(System.currentTimeMillis())); + public double getTargetLow() { + return getTargetLowTimeFromMidnight(secondsFromMidnight(System.currentTimeMillis())); } - public Double getTargetLow(long time) { - return getTargetLow(secondsFromMidnight(time)); + public double getTargetLow(long time) { + return getTargetLowTimeFromMidnight(secondsFromMidnight(time)); } - public Double getTargetLow(Integer timeAsSeconds) { + public double getTargetLowTimeFromMidnight(int timeAsSeconds) { if (targetLow_v == null) targetLow_v = convertToSparseArray(targetLow); return getValueToTime(targetLow_v, timeAsSeconds); } - public Double getTargetHigh() { - return getTargetHigh(secondsFromMidnight(System.currentTimeMillis())); + public double getTargetHigh() { + return getTargetHighTimeFromMidnight(secondsFromMidnight(System.currentTimeMillis())); } - public Double getTargetHigh(long time) { - return getTargetHigh(secondsFromMidnight(time)); + public double getTargetHigh(long time) { + return getTargetHighTimeFromMidnight(secondsFromMidnight(time)); } - public Double getTargetHigh(Integer timeAsSeconds) { + public double getTargetHighTimeFromMidnight(int timeAsSeconds) { if (targetHigh_v == null) targetHigh_v = convertToSparseArray(targetHigh); return getValueToTime(targetHigh_v, timeAsSeconds); } public String getTargetList() { + if (targetLow_v == null) + targetLow_v = convertToSparseArray(targetLow); + if (targetHigh_v == null) + targetHigh_v = convertToSparseArray(targetHigh); return getValuesList(targetLow_v, targetHigh_v, new DecimalFormat("0.0"), getUnits()); } public double getMaxDailyBasal() { - Double max = 0d; - for (Integer hour = 0; hour < 24; hour++) { - double value = getBasal((Integer) (hour * 60 * 60)); + double max = 0d; + for (int hour = 0; hour < 24; hour++) { + double value = getBasalTimeFromMidnight((Integer) (hour * 60 * 60)); if (value > max) max = value; } return max; } - public static Integer secondsFromMidnight() { + public static int secondsFromMidnight() { Calendar c = Calendar.getInstance(); long now = c.getTimeInMillis(); c.set(Calendar.HOUR_OF_DAY, 0); @@ -504,7 +502,7 @@ public class Profile { return (int) (passed / 1000); } - public static Integer secondsFromMidnight(long date) { + public static int secondsFromMidnight(long date) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(date); c.set(Calendar.HOUR_OF_DAY, 0); @@ -515,22 +513,22 @@ public class Profile { return (int) (passed / 1000); } - public static Double toMgdl(Double value, String units) { + public static double toMgdl(double value, String units) { if (units.equals(Constants.MGDL)) return value; else return value * Constants.MMOLL_TO_MGDL; } - public static Double toMmol(Double value, String units) { + public static double toMmol(double value, String units) { if (units.equals(Constants.MGDL)) return value * Constants.MGDL_TO_MMOLL; else return value; } - public static Double fromMgdlToUnits(Double value, String units) { + public static double fromMgdlToUnits(double value, String units) { if (units.equals(Constants.MGDL)) return value; else return value * Constants.MGDL_TO_MMOLL; } - public static Double toUnits(Double valueInMgdl, Double valueInMmol, String units) { + public static double toUnits(Double valueInMgdl, Double valueInMmol, String units) { if (units.equals(Constants.MGDL)) return valueInMgdl; else return valueInMmol; } @@ -556,7 +554,7 @@ public class Profile { public double percentageBasalSum() { double result = 0d; for (int i = 0; i < 24; i++) { - result += getBasal((Integer) (i * 60 * 60)); + result += getBasalTimeFromMidnight(i * 60 * 60); } return result; } @@ -565,7 +563,7 @@ public class Profile { public double baseBasalSum() { double result = 0d; for (int i = 0; i < 24; i++) { - result += getBasal((Integer) (i * 60 * 60)) / getMultiplier(basal_v); + result += getBasalTimeFromMidnight(i * 60 * 60) / getMultiplier(basal_v); } return result; } diff --git a/app/src/main/java/info/nightscout/androidaps/data/ProfileIntervals.java b/app/src/main/java/info/nightscout/androidaps/data/ProfileIntervals.java index 0ea76ffb15..8ea89e2a5b 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/ProfileIntervals.java +++ b/app/src/main/java/info/nightscout/androidaps/data/ProfileIntervals.java @@ -3,10 +3,14 @@ package info.nightscout.androidaps.data; import android.support.annotation.Nullable; import android.support.v4.util.LongSparseArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.List; import info.nightscout.androidaps.interfaces.Interval; +import info.nightscout.utils.DateUtil; /** * Created by mike on 09.05.2017. @@ -16,6 +20,7 @@ import info.nightscout.androidaps.interfaces.Interval; // When no interval match the lastest record without duration is used public class ProfileIntervals { + private static Logger log = LoggerFactory.getLogger(ProfileIntervals.class); private LongSparseArray rawData = new LongSparseArray<>(); // oldest at index 0 @@ -51,6 +56,11 @@ public class ProfileIntervals { public synchronized Interval getValueToTime(long time) { int index = binarySearch(time); if (index >= 0) return rawData.valueAt(index); + // if we request data older than first record, use oldest instead + if (rawData.size() > 0) { + log.debug("Requested profile for time: " + DateUtil.dateAndTimeString(time) + ". Providing oldest record: " + rawData.valueAt(0).toString()); + return rawData.valueAt(0); + } return null; } 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/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index 06e411d856..445e3d7e16 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -8,7 +8,6 @@ import android.support.annotation.Nullable; import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper; import com.j256.ormlite.dao.CloseableIterator; import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.stmt.PreparedQuery; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.Where; @@ -241,7 +240,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { log.error("Unhandled exception", e); } VirtualPumpPlugin.setFakingStatus(true); - scheduleBgChange(null); // trigger refresh + scheduleBgChange(null, false, false); // trigger refresh scheduleTemporaryBasalChange(); scheduleTreatmentChange(null); scheduleExtendedBolusChange(); @@ -367,14 +366,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } // ------------------- BgReading handling ----------------------- - public boolean createIfNotExists(BgReading bgReading, String from) { + public boolean createIfNotExists(BgReading bgReading, String from, boolean isFromActiveBgSource) { try { bgReading.date = roundDateToSec(bgReading.date); BgReading old = getDaoBgReadings().queryForId(bgReading.date); if (old == null) { getDaoBgReadings().create(bgReading); log.debug("BG: New record from: " + from + " " + bgReading.toString()); - scheduleBgChange(bgReading); + scheduleBgChange(bgReading, true, isFromActiveBgSource); return true; } if (!old.isEqual(bgReading)) { @@ -382,7 +381,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { old.copyFrom(bgReading); getDaoBgReadings().update(old); log.debug("BG: Updating record from: " + from + " New data: " + old.toString()); - scheduleBgChange(bgReading); + scheduleBgChange(bgReading, false, isFromActiveBgSource); return false; } } catch (SQLException e) { @@ -400,11 +399,11 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } } - private static void scheduleBgChange(@Nullable final BgReading bgReading) { + private static void scheduleBgChange(@Nullable final BgReading bgReading, boolean isNew, boolean isFromActiveBgSource) { class PostRunnable implements Runnable { public void run() { log.debug("Firing EventNewBg"); - MainApp.bus().post(new EventNewBG(bgReading)); + MainApp.bus().post(new EventNewBG(bgReading, isNew, isFromActiveBgSource)); scheduledBgPost = null; } } @@ -1709,7 +1708,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { if (trJson.has("profileJson")) profileSwitch.profileJson = trJson.getString("profileJson"); else { - ProfileStore store = ConfigBuilderPlugin.getActiveProfileInterface().getProfile(); + ProfileStore store = MainApp.getConfigBuilder().getActiveProfileInterface().getProfile(); Profile profile = store.getSpecificProfile(profileSwitch.profileName); if (profile != null) { profileSwitch.profileJson = profile.getData().toString(); diff --git a/app/src/main/java/info/nightscout/androidaps/events/EventNewBG.java b/app/src/main/java/info/nightscout/androidaps/events/EventNewBG.java index dc4d434e0a..e65c02a58c 100644 --- a/app/src/main/java/info/nightscout/androidaps/events/EventNewBG.java +++ b/app/src/main/java/info/nightscout/androidaps/events/EventNewBG.java @@ -10,8 +10,17 @@ import info.nightscout.androidaps.db.BgReading; public class EventNewBG extends EventLoop { @Nullable public final BgReading bgReading; + public final boolean isNew; + public final boolean isFromActiveBgSource; - public EventNewBG(BgReading bgReading) { + /** Whether the BgReading is current (enough to use for treatment decisions). */ + public boolean isCurrent() { + return bgReading != null && bgReading.date + 9 * 60 * 1000 > System.currentTimeMillis(); + } + + public EventNewBG(@Nullable BgReading bgReading, boolean isNew, boolean isFromActiveBgSource) { this.bgReading = bgReading; + this.isNew = isNew; + this.isFromActiveBgSource = isFromActiveBgSource; } } 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/Actions/ActionsFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Actions/ActionsFragment.java index 0edf29c1e4..8bc79e3ddc 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Actions/ActionsFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Actions/ActionsFragment.java @@ -127,12 +127,14 @@ public class ActionsFragment extends SubscriberFragment implements View.OnClickL return; } final PumpInterface pump = ConfigBuilderPlugin.getActivePump(); - if (!pump.getPumpDescription().isSetBasalProfileCapable || !pump.isInitialized() || pump.isSuspended()) + final boolean basalprofileEnabled = MainApp.isEngineeringModeOrRelease() + && pump.getPumpDescription().isSetBasalProfileCapable; + + if (!basalprofileEnabled || !pump.isInitialized() || pump.isSuspended()) profileSwitch.setVisibility(View.GONE); else profileSwitch.setVisibility(View.VISIBLE); - if (!pump.getPumpDescription().isExtendedBolusCapable || !pump.isInitialized() || pump.isSuspended() || pump.isFakingTempsByExtendedBoluses()) { extendedBolus.setVisibility(View.GONE); extendedBolusCancel.setVisibility(View.GONE); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/CareportalFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/CareportalFragment.java index 701d050cee..2656e43a26 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/CareportalFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/CareportalFragment.java @@ -96,7 +96,7 @@ public class CareportalFragment extends SubscriberFragment implements View.OnCli noProfileView = view.findViewById(R.id.profileview_noprofile); butonsLayout = (LinearLayout) view.findViewById(R.id.careportal_buttons); - ProfileStore profileStore = ConfigBuilderPlugin.getActiveProfileInterface().getProfile(); + ProfileStore profileStore = MainApp.getConfigBuilder().getActiveProfileInterface().getProfile(); if (profileStore == null) { noProfileView.setVisibility(View.VISIBLE); butonsLayout.setVisibility(View.GONE); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/Dialogs/NewNSTreatmentDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/Dialogs/NewNSTreatmentDialog.java index 6e2728c95b..99fc44341b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/Dialogs/NewNSTreatmentDialog.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Careportal/Dialogs/NewNSTreatmentDialog.java @@ -59,6 +59,7 @@ import info.nightscout.utils.FabricPrivacy; import info.nightscout.utils.DateUtil; import info.nightscout.utils.NSUpload; import info.nightscout.utils.NumberPicker; +import info.nightscout.utils.OKDialog; import info.nightscout.utils.SP; import info.nightscout.utils.SafeParse; import info.nightscout.utils.Translator; @@ -73,7 +74,7 @@ public class NewNSTreatmentDialog extends DialogFragment implements View.OnClick Profile profile; ProfileStore profileStore; - String units; + String units = Constants.MGDL; TextView eventTypeText; LinearLayout layoutPercent; @@ -168,19 +169,24 @@ public class NewNSTreatmentDialog extends DialogFragment implements View.OnClick // profile profile = MainApp.getConfigBuilder().getProfile(); - profileStore = ConfigBuilderPlugin.getActiveProfileInterface().getProfile(); - ArrayList profileList; - units = profile != null ? profile.getUnits() : Constants.MGDL; - profileList = profileStore.getProfileList(); - ArrayAdapter adapter = new ArrayAdapter(getContext(), - R.layout.spinner_centered, profileList); - profileSpinner.setAdapter(adapter); - // set selected to actual profile - for (int p = 0; p < profileList.size(); p++) { - if (profileList.get(p).equals(MainApp.getConfigBuilder().getProfileName(false))) - profileSpinner.setSelection(p); + profileStore = MainApp.getConfigBuilder().getActiveProfileInterface().getProfile(); + if (profileStore == null) { + if (options.eventType == R.id.careportal_profileswitch) { + log.error("Profile switch called but plugin doesn't contain valid profile"); + } + } else { + ArrayList profileList; + units = profile != null ? profile.getUnits() : Constants.MGDL; + profileList = profileStore.getProfileList(); + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), + R.layout.spinner_centered, profileList); + profileSpinner.setAdapter(adapter); + // set selected to actual profile + for (int p = 0; p < profileList.size(); p++) { + if (profileList.get(p).equals(MainApp.getConfigBuilder().getProfileName(false))) + profileSpinner.setSelection(p); + } } - final Double bg = Profile.fromMgdlToUnits(GlucoseStatus.getGlucoseStatusData() != null ? GlucoseStatus.getGlucoseStatusData().glucose : 0d, units); // temp target @@ -720,7 +726,7 @@ public class NewNSTreatmentDialog extends DialogFragment implements View.OnClick profileSwitch.source = Source.USER; profileSwitch.profileName = profileName; profileSwitch.profileJson = profileStore.getSpecificProfile(profileName).getData().toString(); - profileSwitch.profilePlugin = ConfigBuilderPlugin.getActiveProfileInterface().getClass().getName(); + profileSwitch.profilePlugin = MainApp.getConfigBuilder().getActiveProfileInterface().getClass().getName(); profileSwitch.durationInMinutes = duration; profileSwitch.isCPP = percentage != 100 || timeshift != 0; profileSwitch.timeshift = timeshift; @@ -752,7 +758,7 @@ public class NewNSTreatmentDialog extends DialogFragment implements View.OnClick profileSwitch.source = Source.USER; profileSwitch.profileName = MainApp.getConfigBuilder().getProfileName(System.currentTimeMillis(), false); profileSwitch.profileJson = MainApp.getConfigBuilder().getProfile().getData().toString(); - profileSwitch.profilePlugin = ConfigBuilderPlugin.getActiveProfileInterface().getClass().getName(); + profileSwitch.profilePlugin = MainApp.getConfigBuilder().getActiveProfileInterface().getClass().getName(); profileSwitch.durationInMinutes = duration; profileSwitch.isCPP = percentage != 100 || timeshift != 0; profileSwitch.timeshift = timeshift; 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 1b411b3c40..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; @@ -200,7 +201,7 @@ public class ConfigBuilderPlugin implements PluginBase, ConstraintsInterface, Tr return activeBgSource; } - public static ProfileInterface getActiveProfileInterface() { + public ProfileInterface getActiveProfileInterface() { return activeProfile; } @@ -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); } @@ -760,33 +762,21 @@ public class ConfigBuilderPlugin implements PluginBase, ConstraintsInterface, Tr } public String getProfileName(long time, boolean customized) { - boolean ignoreProfileSwitchEvents = SP.getBoolean(R.string.key_do_not_track_profile_switch, false); - if (!ignoreProfileSwitchEvents) { - ProfileSwitch profileSwitch = getProfileSwitchFromHistory(time); - if (profileSwitch != null) { - if (profileSwitch.profileJson != null) { - return customized ? profileSwitch.getCustomizedName() : profileSwitch.profileName; - } else { - Profile profile = activeProfile.getProfile().getSpecificProfile(profileSwitch.profileName); - if (profile != null) - return profileSwitch.profileName; - } + ProfileSwitch profileSwitch = getProfileSwitchFromHistory(time); + if (profileSwitch != null) { + if (profileSwitch.profileJson != null) { + return customized ? profileSwitch.getCustomizedName() : profileSwitch.profileName; + } else { + Profile profile = activeProfile.getProfile().getSpecificProfile(profileSwitch.profileName); + if (profile != null) + return profileSwitch.profileName; } } - // Unable to determine profile, failover to default - String defaultProfile = activeProfile.getProfile().getDefaultProfileName(); - if (defaultProfile != null) - return defaultProfile; - // If default from plugin fails .... create empty - return "Default"; + return MainApp.gs(R.string.noprofileselected); } public boolean isProfileValid(String from) { - return getProfile() != null && getProfile().isValid(from) && - activeProfile != null && - activeProfile.getProfile() != null && - activeProfile.getProfile().getDefaultProfile() != null && - activeProfile.getProfile().getDefaultProfile().isValid(from); + return getProfile() != null && getProfile().isValid(from); } @Nullable @@ -806,41 +796,16 @@ public class ConfigBuilderPlugin implements PluginBase, ConstraintsInterface, Tr return null; //app not initialized } //log.debug("Profile for: " + new Date(time).toLocaleString() + " : " + getProfileName(time)); - boolean ignoreProfileSwitchEvents = SP.getBoolean(R.string.key_do_not_track_profile_switch, false); - if (!ignoreProfileSwitchEvents) { - ProfileSwitch profileSwitch = getProfileSwitchFromHistory(time); - if (profileSwitch != null) { - if (profileSwitch.profileJson != null) { - return profileSwitch.getProfileObject(); - } else if (activeProfile.getProfile() != null) { - Profile profile = activeProfile.getProfile().getSpecificProfile(profileSwitch.profileName); - if (profile != null) - return profile; - } + ProfileSwitch profileSwitch = getProfileSwitchFromHistory(time); + if (profileSwitch != null) { + if (profileSwitch.profileJson != null) { + return profileSwitch.getProfileObject(); + } else if (activeProfile.getProfile() != null) { + Profile profile = activeProfile.getProfile().getSpecificProfile(profileSwitch.profileName); + if (profile != null) + return profile; } } - // Unable to determine profile, failover to default - if (activeProfile.getProfile() == null) { - log.debug("getProfile activeProfile.getProfile() == null: returning null (activeProfile=" + activeProfile.getClass().getSimpleName() + ")"); - return null; //app not initialized - } - Profile defaultProfile = activeProfile.getProfile().getDefaultProfile(); - if (defaultProfile != null) - return defaultProfile; - // If default from plugin fails .... create empty - try { - Notification noisf = new Notification(Notification.ISF_MISSING, MainApp.sResources.getString(R.string.isfmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(noisf)); - Notification noic = new Notification(Notification.IC_MISSING, MainApp.sResources.getString(R.string.icmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(noic)); - Notification nobasal = new Notification(Notification.BASAL_MISSING, MainApp.sResources.getString(R.string.basalmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(nobasal)); - Notification notarget = new Notification(Notification.TARGET_MISSING, MainApp.sResources.getString(R.string.targetmissing), Notification.URGENT); - MainApp.bus().post(new EventNewNotification(notarget)); - return new Profile(new JSONObject("{\"dia\":\"3\",\"carbratio\":[{\"time\":\"00:00\",\"value\":\"20\"}],\"carbs_hr\":\"20\",\"delay\":\"20\",\"sens\":[{\"time\":\"00:00\",\"value\":\"20\"}],\"timezone\":\"UTC\",\"basal\":[{\"time\":\"00:00\",\"value\":\"0.1\"}],\"target_low\":[{\"time\":\"00:00\",\"value\":\"6\"}],\"target_high\":[{\"time\":\"00:00\",\"value\":\"8\"}],\"startDate\":\"1970-01-01T00:00:00.000Z\",\"units\":\"mmol\"}}"), 100, 0); - } catch (JSONException e) { - log.error("Unhandled exception", e); - } log.debug("getProfile at the end: returning null"); return null; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/ConstraintsSafety/SafetyPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/ConstraintsSafety/SafetyPlugin.java index ef30a0ebb9..41a7c1eb00 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/ConstraintsSafety/SafetyPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/ConstraintsSafety/SafetyPlugin.java @@ -102,6 +102,7 @@ public class SafetyPlugin implements PluginBase, ConstraintsInterface { **/ @Override public boolean isClosedModeEnabled() { + if (!MainApp.isEngineeringModeOrRelease()) return false; String mode = SP.getString("aps_mode", "open"); return mode.equals("closed") && BuildConfig.CLOSEDLOOP; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobCalculatorPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobCalculatorPlugin.java index c60991253c..19b8e492d6 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobCalculatorPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobCalculatorPlugin.java @@ -531,6 +531,8 @@ public class IobCobCalculatorPlugin implements PluginBase { @Subscribe public void onEventNewBG(EventNewBG ev) { + if (!ev.isFromActiveBgSource) + return; if (this != getPlugin()) { log.debug("Ignoring event for non default instance"); return; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobThread.java b/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobThread.java index c492aae400..1a4c499c59 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobThread.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/IobCobCalculator/IobCobThread.java @@ -110,11 +110,6 @@ public class IobCobThread extends Thread { return; // profile not set yet } - if (profile.getIsf(bgTime) == null) { - log.debug("Aborting calculation thread (no ISF): " + from); - return; // profile not set yet - } - if (Config.logAutosensData) log.debug("Processing calculation thread: " + from + " (" + i + "/" + bucketed_data.size() + ")"); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Loop/LoopPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/Loop/LoopPlugin.java index 12db67534b..5846140b1e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Loop/LoopPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Loop/LoopPlugin.java @@ -23,6 +23,7 @@ import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; import info.nightscout.androidaps.data.Profile; import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.BgReading; import info.nightscout.androidaps.events.EventNewBG; import info.nightscout.androidaps.events.EventTreatmentChange; import info.nightscout.androidaps.interfaces.APSInterface; @@ -159,8 +160,12 @@ public class LoopPlugin implements PluginBase { @Subscribe public void onStatusEvent(final EventAutosensCalculationFinished ev) { - if (ev.cause instanceof EventNewBG) { - invoke(ev.getClass().getSimpleName() + "(" + ev.cause.getClass().getSimpleName() + ")", true); + if (!(ev.cause instanceof EventNewBG)) + return; + + EventNewBG bgEv = (EventNewBG) ev.cause; + if (bgEv.isNew && bgEv.isFromActiveBgSource && bgEv.isCurrent()) { + invoke("New BG", true); } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java index 5372f66b03..5fc7809f4e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java @@ -204,7 +204,7 @@ public class DetermineBasalAdapterAMAJS { mProfile.put("max_bg", maxBg); mProfile.put("target_bg", targetBg); mProfile.put("carb_ratio", profile.getIc()); - mProfile.put("sens", Profile.toMgdl(profile.getIsf().doubleValue(), units)); + mProfile.put("sens", Profile.toMgdl(profile.getIsf(), units)); mProfile.put("max_daily_safety_multiplier", SP.getInt("openapsama_max_daily_safety_multiplier", 3)); mProfile.put("current_basal_safety_multiplier", SP.getDouble("openapsama_current_basal_safety_multiplier", 4d)); mProfile.put("skip_neutral_temps", true); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java index 6ac207ab80..dfab195125 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java @@ -212,9 +212,9 @@ public class OpenAPSAMAPlugin implements PluginBase, APSInterface { if (!HardLimits.checkOnlyHardLimits(profile.getDia(), "dia", HardLimits.MINDIA, HardLimits.MAXDIA)) return; - if (!HardLimits.checkOnlyHardLimits(profile.getIc(profile.secondsFromMidnight()), "carbratio", HardLimits.MINIC, HardLimits.MAXIC)) + if (!HardLimits.checkOnlyHardLimits(profile.getIcTimeFromMidnight(profile.secondsFromMidnight()), "carbratio", HardLimits.MINIC, HardLimits.MAXIC)) return; - if (!HardLimits.checkOnlyHardLimits(Profile.toMgdl(profile.getIsf().doubleValue(), units), "sens", HardLimits.MINISF, HardLimits.MAXISF)) + if (!HardLimits.checkOnlyHardLimits(Profile.toMgdl(profile.getIsf(), units), "sens", HardLimits.MINISF, HardLimits.MAXISF)) return; if (!HardLimits.checkOnlyHardLimits(profile.getMaxDailyBasal(), "max_daily_basal", 0.05, HardLimits.maxBasal())) return; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterMAJS.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterMAJS.java index e7bd0c86fd..1ffa682fc5 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterMAJS.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterMAJS.java @@ -172,7 +172,7 @@ public class DetermineBasalAdapterMAJS { mProfile.put("max_bg", maxBg); mProfile.put("target_bg", targetBg); mProfile.put("carb_ratio", profile.getIc()); - mProfile.put("sens", Profile.toMgdl(profile.getIsf().doubleValue(), units)); + mProfile.put("sens", Profile.toMgdl(profile.getIsf(), units)); mProfile.put("current_basal", basalRate); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAPlugin.java index 7f8b56f021..76df63e752 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAPlugin.java @@ -212,9 +212,9 @@ public class OpenAPSMAPlugin implements PluginBase, APSInterface { if (!checkOnlyHardLimits(profile.getDia(), "dia", HardLimits.MINDIA, HardLimits.MAXDIA)) return; - if (!checkOnlyHardLimits(profile.getIc(profile.secondsFromMidnight()), "carbratio", HardLimits.MINIC, HardLimits.MAXIC)) + if (!checkOnlyHardLimits(profile.getIcTimeFromMidnight(profile.secondsFromMidnight()), "carbratio", HardLimits.MINIC, HardLimits.MAXIC)) return; - if (!checkOnlyHardLimits(Profile.toMgdl(profile.getIsf().doubleValue(), units), "sens", HardLimits.MINISF, HardLimits.MAXISF)) + if (!checkOnlyHardLimits(Profile.toMgdl(profile.getIsf(), units), "sens", HardLimits.MINISF, HardLimits.MAXISF)) return; if (!checkOnlyHardLimits(profile.getMaxDailyBasal(), "max_daily_basal", 0.05, HardLimits.maxBasal())) return; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/DetermineBasalAdapterSMBJS.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/DetermineBasalAdapterSMBJS.java index 64f8a155fd..9df54b34d3 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/DetermineBasalAdapterSMBJS.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/DetermineBasalAdapterSMBJS.java @@ -228,7 +228,7 @@ public class DetermineBasalAdapterSMBJS { mProfile.put("max_bg", maxBg); mProfile.put("target_bg", targetBg); mProfile.put("carb_ratio", profile.getIc()); - mProfile.put("sens", Profile.toMgdl(profile.getIsf().doubleValue(), units)); + mProfile.put("sens", Profile.toMgdl(profile.getIsf(), units)); mProfile.put("max_daily_safety_multiplier", SP.getInt("openapsama_max_daily_safety_multiplier", 3)); mProfile.put("current_basal_safety_multiplier", SP.getDouble("openapsama_current_basal_safety_multiplier", 4d)); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/OpenAPSSMBPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/OpenAPSSMBPlugin.java index 91aa4ef515..f10e6cb0be 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/OpenAPSSMBPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSSMB/OpenAPSSMBPlugin.java @@ -216,9 +216,9 @@ public class OpenAPSSMBPlugin implements PluginBase, APSInterface { maxBasal = verifyHardLimits(maxBasal, "max_basal", 0.1, HardLimits.maxBasal()); if (!checkOnlyHardLimits(profile.getDia(), "dia", HardLimits.MINDIA, HardLimits.MAXDIA)) return; - if (!checkOnlyHardLimits(profile.getIc(profile.secondsFromMidnight()), "carbratio", HardLimits.MINIC, HardLimits.MAXIC)) + if (!checkOnlyHardLimits(profile.getIcTimeFromMidnight(profile.secondsFromMidnight()), "carbratio", HardLimits.MINIC, HardLimits.MAXIC)) return; - if (!checkOnlyHardLimits(Profile.toMgdl(profile.getIsf().doubleValue(), units), "sens", HardLimits.MINISF, HardLimits.MAXISF)) + if (!checkOnlyHardLimits(Profile.toMgdl(profile.getIsf(), units), "sens", HardLimits.MINISF, HardLimits.MAXISF)) return; if (!checkOnlyHardLimits(profile.getMaxDailyBasal(), "max_daily_basal", 0.05, HardLimits.maxBasal())) return; if (!checkOnlyHardLimits(pump.getBaseBasalRate(), "current_basal", 0.01, HardLimits.maxBasal())) return; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java index e99248c966..0d59983796 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java @@ -142,14 +142,16 @@ public class WizardDialog extends DialogFragment implements OnClickListener, Com @Subscribe public void onStatusEvent(final EventNewBG e) { - Activity activity = getActivity(); - if (activity != null) - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - calculateInsulin(); - } - }); + if (e.isFromActiveBgSource && e.isNew && e.isCurrent()) { + Activity activity = getActivity(); + if (activity != null) + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + calculateInsulin(); + } + }); + } } @Subscribe @@ -400,7 +402,7 @@ public class WizardDialog extends DialogFragment implements OnClickListener, Com private void initDialog() { Profile profile = MainApp.getConfigBuilder().getProfile(); - ProfileStore profileStore = ConfigBuilderPlugin.getActiveProfileInterface().getProfile(); + ProfileStore profileStore = MainApp.getConfigBuilder().getActiveProfileInterface().getProfile(); if (profile == null) { ToastUtils.showToastInUiThread(MainApp.instance().getApplicationContext(), MainApp.sResources.getString(R.string.noprofile)); @@ -444,7 +446,7 @@ public class WizardDialog extends DialogFragment implements OnClickListener, Com } private void calculateInsulin() { - ProfileStore profileStore = ConfigBuilderPlugin.getActiveProfileInterface().getProfile(); + ProfileStore profileStore = MainApp.getConfigBuilder().getActiveProfileInterface().getProfile(); if (profileSpinner == null || profileSpinner.getSelectedItem() == null || profileStore == null) return; // not initialized yet String selectedAlternativeProfile = profileSpinner.getSelectedItem().toString(); 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 3dd6b04f40..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 @@ -187,7 +187,8 @@ public class OverviewFragment extends Fragment implements View.OnClickListener, final Object updateSync = new Object(); - public enum CHARTTYPE {PRE,BAS, IOB, COB, DEV, SEN}; + public enum CHARTTYPE {PRE, BAS, IOB, COB, DEV, SEN}; + private static final ScheduledExecutorService worker = Executors.newSingleThreadScheduledExecutor(); private static ScheduledFuture scheduledUpdate = null; @@ -344,7 +345,7 @@ public class OverviewFragment extends Fragment implements View.OnClickListener, SpannableString s; PopupMenu popup = new PopupMenu(v.getContext(), v); - if(predictionsAvailable) { + if (predictionsAvailable) { item = popup.getMenu().add(Menu.NONE, CHARTTYPE.PRE.ordinal(), Menu.NONE, "Predictions"); title = item.getTitle(); s = new SpannableString(title); @@ -464,7 +465,9 @@ public class OverviewFragment extends Fragment implements View.OnClickListener, } else if (v == activeProfileView) { menu.setHeaderTitle(MainApp.sResources.getString(R.string.profile)); menu.add(MainApp.sResources.getString(R.string.danar_viewprofile)); - menu.add(MainApp.sResources.getString(R.string.careportal_profileswitch)); + if (MainApp.getConfigBuilder().getActiveProfileInterface().getProfile() != null) { + menu.add(MainApp.sResources.getString(R.string.careportal_profileswitch)); + } } } @@ -966,7 +969,7 @@ public class OverviewFragment extends Fragment implements View.OnClickListener, if (timeView != null) { //must not exists timeView.setText(DateUtil.timeString(new Date())); } - if (!MainApp.getConfigBuilder().isProfileValid("Overview")) {// app not initialized yet + if (!MainApp.getConfigBuilder().isProfileValid("Overview")) { pumpStatusView.setText(R.string.noprofileset); pumpStatusLayout.setVisibility(View.VISIBLE); loopStatusLayout.setVisibility(View.GONE); @@ -983,12 +986,6 @@ public class OverviewFragment extends Fragment implements View.OnClickListener, final PumpInterface pump = ConfigBuilderPlugin.getActivePump(); final Profile profile = MainApp.getConfigBuilder().getProfile(); - if (profile == null) { - pumpStatusView.setText(R.string.noprofileset); - pumpStatusLayout.setVisibility(View.VISIBLE); - loopStatusLayout.setVisibility(View.GONE); - return; - } final String units = profile.getUnits(); final double lowLine = OverviewPlugin.getPlugin().determineLowLine(units); @@ -1244,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 7f8b94cfa9..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 @@ -42,10 +42,7 @@ public class Notification { public static final int APPROACHING_DAILY_LIMIT = 11; public static final int NSCLIENT_NO_WRITE_PERMISSION = 12; public static final int MISSING_SMS_PERMISSION = 13; - public static final int ISF_MISSING = 14; - public static final int IC_MISSING = 15; - public static final int BASAL_MISSING = 16; - public static final int TARGET_MISSING = 17; + public static final int NSANNOUNCEMENT = 18; public static final int NSALARM = 19; public static final int NSURGENTALARM = 20; @@ -53,12 +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 PROFILE_SWITCH_MISSING = 32; + public static final int NOT_ENG_MODE_OR_RELEASE = 33; public int id; public Date date; @@ -203,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/Persistentnotification/PersistentNotificationPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/Persistentnotification/PersistentNotificationPlugin.java index 26d5851865..a68864b9c8 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Persistentnotification/PersistentNotificationPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Persistentnotification/PersistentNotificationPlugin.java @@ -255,7 +255,8 @@ public class PersistentNotificationPlugin implements PluginBase { @Subscribe public void onStatusEvent(final EventNewBG ev) { - updateNotification(); + if (ev.isFromActiveBgSource && ev.isNew && ev.isCurrent()) + updateNotification(); } @Subscribe diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfileFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfileFragment.java index 84a1dd3a87..b99a9f0799 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfileFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfileFragment.java @@ -14,47 +14,55 @@ import com.squareup.otto.Subscribe; import java.util.ArrayList; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnItemSelected; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; import info.nightscout.androidaps.data.Profile; import info.nightscout.androidaps.data.ProfileStore; import info.nightscout.androidaps.plugins.Common.SubscriberFragment; import info.nightscout.androidaps.plugins.ProfileNS.events.EventNSProfileUpdateGUI; +import info.nightscout.androidaps.plugins.Treatments.fragments.ProfileGraph; import info.nightscout.utils.DecimalFormatter; import info.nightscout.utils.FabricPrivacy; +import static butterknife.OnItemSelected.Callback.NOTHING_SELECTED; -public class NSProfileFragment extends SubscriberFragment implements AdapterView.OnItemSelectedListener { - private Spinner profileSpinner; - private TextView noProfile; - private TextView units; - private TextView dia; - private TextView activeProfile; - private TextView ic; - private TextView isf; - private TextView basal; - private TextView target; + +public class NSProfileFragment extends SubscriberFragment { + @BindView(R.id.nsprofile_spinner) + Spinner profileSpinner; + @BindView(R.id.profileview_noprofile) + TextView noProfile; + @BindView(R.id.profileview_invalidprofile) + TextView invalidProfile; + @BindView(R.id.profileview_units) + TextView units; + @BindView(R.id.profileview_dia) + TextView dia; + @BindView(R.id.profileview_activeprofile) + TextView activeProfile; + @BindView(R.id.profileview_ic) + TextView ic; + @BindView(R.id.profileview_isf) + TextView isf; + @BindView(R.id.profileview_basal) + TextView basal; + @BindView(R.id.profileview_target) + TextView target; + @BindView(R.id.basal_graph) + ProfileGraph basalGraph; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { try { - View layout = inflater.inflate(R.layout.nsprofile_fragment, container, false); - - profileSpinner = (Spinner) layout.findViewById(R.id.nsprofile_spinner); - noProfile = (TextView) layout.findViewById(R.id.profileview_noprofile); - units = (TextView) layout.findViewById(R.id.profileview_units); - dia = (TextView) layout.findViewById(R.id.profileview_dia); - activeProfile = (TextView) layout.findViewById(R.id.profileview_activeprofile); - ic = (TextView) layout.findViewById(R.id.profileview_ic); - isf = (TextView) layout.findViewById(R.id.profileview_isf); - basal = (TextView) layout.findViewById(R.id.profileview_basal); - target = (TextView) layout.findViewById(R.id.profileview_target); - - profileSpinner.setOnItemSelectedListener(this); + View view = inflater.inflate(R.layout.nsprofile_fragment, container, false); + unbinder = ButterKnife.bind(this, view); updateGUI(); - return layout; + return view; } catch (Exception e) { FabricPrivacy.logException(e); } @@ -66,12 +74,7 @@ public class NSProfileFragment extends SubscriberFragment implements AdapterView public void onStatusEvent(final EventNSProfileUpdateGUI ev) { Activity activity = getActivity(); if (activity != null) - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - updateGUI(); - } - }); + activity.runOnUiThread(() -> updateGUI()); } @Override @@ -97,9 +100,9 @@ public class NSProfileFragment extends SubscriberFragment implements AdapterView } } - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - String name = parent.getItemAtPosition(position).toString(); + @OnItemSelected(R.id.nsprofile_spinner) + public void onItemSelected(Spinner spinner, int position) { + String name = spinner.getItemAtPosition(position).toString(); ProfileStore store = NSProfilePlugin.getPlugin().getProfile(); if (store != null) { @@ -112,12 +115,18 @@ public class NSProfileFragment extends SubscriberFragment implements AdapterView isf.setText(profile.getIsfList()); basal.setText(profile.getBasalList()); target.setText(profile.getTargetList()); + basalGraph.show(profile); } + if (profile.isValid("NSProfileFragment")) + invalidProfile.setVisibility(View.GONE); + else + invalidProfile.setVisibility(View.VISIBLE); } } - @Override - public void onNothingSelected(AdapterView parent) { + @OnItemSelected(value = R.id.nsprofile_spinner, callback = NOTHING_SELECTED) + public void onNothingSelected() { + invalidProfile.setVisibility(View.VISIBLE); noProfile.setVisibility(View.VISIBLE); units.setText(""); dia.setText(""); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfilePlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfilePlugin.java index 4911187db4..c2d7cfabfc 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfilePlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/ProfileNS/NSProfilePlugin.java @@ -120,19 +120,6 @@ public class NSProfilePlugin implements PluginBase, ProfileInterface { profile = new ProfileStore(newProfile.getData()); storeNSProfile(); MainApp.bus().post(new EventNSProfileUpdateGUI()); - if (MainApp.getConfigBuilder().isProfileValid("storeNewProfile")) { - ConfigBuilderPlugin.getCommandQueue().setProfile(MainApp.getConfigBuilder().getProfile(), new Callback() { - @Override - public void run() { - if (result.enacted) { - SmsCommunicatorPlugin smsCommunicatorPlugin = MainApp.getSpecificPlugin(SmsCommunicatorPlugin.class); - if (smsCommunicatorPlugin != null && smsCommunicatorPlugin.isEnabled(PluginBase.GENERAL)) { - smsCommunicatorPlugin.sendNotificationToAllNumbers(MainApp.sResources.getString(R.string.profile_set_ok)); - } - } - } - }); - } } private void storeNSProfile() { 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/PumpDanaR/AbstractDanaRPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaR/AbstractDanaRPlugin.java index 758f872ffe..b57b8543f4 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaR/AbstractDanaRPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaR/AbstractDanaRPlugin.java @@ -184,7 +184,7 @@ public abstract class AbstractDanaRPlugin implements PluginBase, PumpInterface, int basalIncrement = pump.basal48Enable ? 30 * 60 : 60 * 60; for (int h = 0; h < basalValues; h++) { Double pumpValue = pump.pumpProfiles[pump.activeProfile][h]; - Double profileValue = profile.getBasal((Integer) (h * basalIncrement)); + Double profileValue = profile.getBasalTimeFromMidnight((Integer) (h * basalIncrement)); if (profileValue == null) return true; if (Math.abs(pumpValue - profileValue) > getPumpDescription().basalStep) { log.debug("Diff found. Hour: " + h + " Pump: " + pumpValue + " Profile: " + profileValue); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaR/DanaRPump.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaR/DanaRPump.java index 520e12d454..1e346695e3 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaR/DanaRPump.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaR/DanaRPump.java @@ -241,7 +241,7 @@ public class DanaRPump { for (Integer hour = 0; hour < 24; hour++) { //Some values get truncated to the next lower one. // -> round them to two decimals and make sure we are a small delta larger (that will get truncated) - double value = Math.round(100d * nsProfile.getBasal((Integer) (hour * 60 * 60)))/100d + 0.00001; + double value = Math.round(100d * nsProfile.getBasalTimeFromMidnight((Integer) (hour * 60 * 60)))/100d + 0.00001; if (Config.logDanaMessageDetail) log.debug("NS basal value for " + hour + ":00 is " + value); record[hour] = value; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaRS/DanaRSPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaRS/DanaRSPlugin.java index 437e528441..48838117c6 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaRS/DanaRSPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpDanaRS/DanaRSPlugin.java @@ -428,7 +428,7 @@ public class DanaRSPlugin implements PluginBase, PumpInterface, DanaRInterface, int basalIncrement = pump.basal48Enable ? 30 * 60 : 60 * 60; for (int h = 0; h < basalValues; h++) { Double pumpValue = pump.pumpProfiles[pump.activeProfile][h]; - Double profileValue = profile.getBasal((Integer) (h * basalIncrement)); + Double profileValue = profile.getBasalTimeFromMidnight((Integer) (h * basalIncrement)); if (profileValue == null) return true; if (Math.abs(pumpValue - profileValue) > getPumpDescription().basalStep) { log.debug("Diff found. Hour: " + h + " Pump: " + pumpValue + " Profile: " + profileValue); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpVirtual/VirtualPumpPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpVirtual/VirtualPumpPlugin.java index 46e90184fc..0a38477808 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PumpVirtual/VirtualPumpPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpVirtual/VirtualPumpPlugin.java @@ -52,12 +52,12 @@ public class VirtualPumpPlugin implements PluginBase, PumpInterface { private PumpDescription pumpDescription = new PumpDescription(); private static void loadFakingStatus() { - fromNSAreCommingFakedExtendedBoluses = SP.getBoolean("fromNSAreCommingFakedExtendedBoluses", false); + fromNSAreCommingFakedExtendedBoluses = SP.getBoolean(R.string.key_fromNSAreCommingFakedExtendedBoluses, false); } public static void setFakingStatus(boolean newStatus) { fromNSAreCommingFakedExtendedBoluses = newStatus; - SP.putBoolean("fromNSAreCommingFakedExtendedBoluses", fromNSAreCommingFakedExtendedBoluses); + SP.putBoolean(R.string.key_fromNSAreCommingFakedExtendedBoluses, fromNSAreCommingFakedExtendedBoluses); } public static boolean getFakingStatus() { @@ -73,7 +73,7 @@ public class VirtualPumpPlugin implements PluginBase, PumpInterface { return plugin; } - private VirtualPumpPlugin() { + public VirtualPumpPlugin() { pumpDescription.isBolusCapable = true; pumpDescription.bolusStep = 0.1d; @@ -245,7 +245,7 @@ public class VirtualPumpPlugin implements PluginBase, PumpInterface { public double getBaseBasalRate() { Profile profile = MainApp.getConfigBuilder().getProfile(); if (profile != null) - return profile.getBasal() != null ? profile.getBasal() : 0d; + return profile.getBasal(); else return 0d; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/SourceDexcomG5/BGSourceFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/SourceDexcomG5/BGSourceFragment.java index 4b3208a2bf..ef5c4bce5b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/SourceDexcomG5/BGSourceFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/SourceDexcomG5/BGSourceFragment.java @@ -59,7 +59,7 @@ public class BGSourceFragment extends SubscriberFragment { RecyclerViewAdapter adapter = new RecyclerViewAdapter(MainApp.getDbHelper().getAllBgreadingsDataFromTime(now - MILLS_TO_THE_PAST, false)); recyclerView.setAdapter(adapter); - profile = ConfigBuilderPlugin.getActiveProfileInterface().getProfile().getDefaultProfile(); + profile = MainApp.getConfigBuilder().getActiveProfileInterface().getProfile().getDefaultProfile(); return view; } catch (Exception e) { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileGraph.java b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileGraph.java index b38fe4001e..61f4877ccc 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileGraph.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileGraph.java @@ -33,8 +33,8 @@ public class ProfileGraph extends GraphView { List basalArray = new ArrayList<>(); for (int hour = 0; hour < 24; hour++) { - basalArray.add(new DataPoint(hour, profile.getBasal(new Integer(hour*60*60)))); - basalArray.add(new DataPoint(hour+1, profile.getBasal(new Integer(hour*60*60)))); + basalArray.add(new DataPoint(hour, profile.getBasalTimeFromMidnight(new Integer(hour*60*60)))); + basalArray.add(new DataPoint(hour+1, profile.getBasalTimeFromMidnight(new Integer(hour*60*60)))); } DataPoint[] basalDataPoints = new DataPoint[basalArray.size()]; basalDataPoints = basalArray.toArray(basalDataPoints); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileViewerDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileViewerDialog.java index fa1105faac..3286f91a62 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileViewerDialog.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/fragments/ProfileViewerDialog.java @@ -12,6 +12,9 @@ import android.widget.TextView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; import info.nightscout.androidaps.data.Profile; @@ -30,19 +33,36 @@ public class ProfileViewerDialog extends DialogFragment { private static Logger log = LoggerFactory.getLogger(ProfileViewDialog.class); - private TextView noProfile; - private TextView units; - private TextView dia; - private TextView activeProfile; - private TextView ic; - private TextView isf; - private TextView basal; - private TextView target; - private View dateDelimiter; - private LinearLayout dateLayout; - private TextView dateTextView; - private Button refreshButton; - private ProfileGraph basalGraph; + @BindView(R.id.profileview_noprofile) + TextView noProfile; + @BindView(R.id.profileview_invalidprofile) + TextView invalidProfile; + @BindView(R.id.profileview_units) + TextView units; + @BindView(R.id.profileview_dia) + TextView dia; + @BindView(R.id.profileview_activeprofile) + TextView activeProfile; + @BindView(R.id.profileview_ic) + TextView ic; + @BindView(R.id.profileview_isf) + TextView isf; + @BindView(R.id.profileview_basal) + TextView basal; + @BindView(R.id.profileview_target) + TextView target; + @BindView(R.id.profileview_datedelimiter) + View dateDelimiter; + @BindView(R.id.profileview_datelayout) + LinearLayout dateLayout; + @BindView(R.id.profileview_date) + TextView dateTextView; + @BindView(R.id.profileview_reload) + Button refreshButton; + @BindView(R.id.basal_graph) + ProfileGraph basalGraph; + + private Unbinder unbinder; public static ProfileViewerDialog newInstance(long time) { ProfileViewerDialog dialog = new ProfileViewerDialog(); @@ -61,31 +81,26 @@ public class ProfileViewerDialog extends DialogFragment { time = getArguments().getLong("time"); } + @Override + public void onDestroyView() { + super.onDestroyView(); + if (unbinder != null) + unbinder.unbind(); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.profileviewer_fragment, container, false); + View view = inflater.inflate(R.layout.profileviewer_fragment, container, false); + unbinder = ButterKnife.bind(this, view); - noProfile = (TextView) layout.findViewById(R.id.profileview_noprofile); - units = (TextView) layout.findViewById(R.id.profileview_units); - dia = (TextView) layout.findViewById(R.id.profileview_dia); - activeProfile = (TextView) layout.findViewById(R.id.profileview_activeprofile); - ic = (TextView) layout.findViewById(R.id.profileview_ic); - isf = (TextView) layout.findViewById(R.id.profileview_isf); - basal = (TextView) layout.findViewById(R.id.profileview_basal); - target = (TextView) layout.findViewById(R.id.profileview_target); - refreshButton = (Button) layout.findViewById(R.id.profileview_reload); refreshButton.setVisibility(View.GONE); - dateDelimiter = layout.findViewById(R.id.profileview_datedelimiter); dateDelimiter.setVisibility(View.VISIBLE); - dateLayout = (LinearLayout) layout.findViewById(R.id.profileview_datelayout); dateLayout.setVisibility(View.VISIBLE); - dateTextView = (TextView) layout.findViewById(R.id.profileview_date); - basalGraph = (ProfileGraph) layout.findViewById(R.id.basal_graph); setContent(); - return layout; + return view; } @Override @@ -114,6 +129,11 @@ public class ProfileViewerDialog extends DialogFragment { basal.setText(profile.getBasalList()); target.setText(profile.getTargetList()); basalGraph.show(profile); + + if (profile.isValid("ProfileViewDialog")) + invalidProfile.setVisibility(View.GONE); + else + invalidProfile.setVisibility(View.VISIBLE); } else { noProfile.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Wear/ActionStringHandler.java b/app/src/main/java/info/nightscout/androidaps/plugins/Wear/ActionStringHandler.java index 30d0060682..b304490e22 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Wear/ActionStringHandler.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Wear/ActionStringHandler.java @@ -586,7 +586,7 @@ public class ActionStringHandler { } final Profile profile = MainApp.getConfigBuilder().getProfile(); - if (profile == null || profile.getBasal() == null) { + if (profile == null) { msg += MainApp.sResources.getString(R.string.notloadedplugins) + "\n"; } if (!"".equals(msg)) { 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..bc44c75911 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; @@ -182,7 +182,8 @@ public class WearPlugin implements PluginBase { @Subscribe public void onStatusEvent(final EventNewBG ev) { - sendDataToWatch(true, true, true); + if (ev.isFromActiveBgSource) + sendDataToWatch(true, true, true); } @Subscribe diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/XDripStatusline/StatuslinePlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/XDripStatusline/StatuslinePlugin.java index 76e8ac9e4d..f1fd58c3f9 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/XDripStatusline/StatuslinePlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/XDripStatusline/StatuslinePlugin.java @@ -235,7 +235,8 @@ public class StatuslinePlugin implements PluginBase { @Subscribe public void onStatusEvent(final EventNewBG ev) { - sendStatus(); + if (ev.isFromActiveBgSource && ev.isNew && ev.isCurrent()) + sendStatus(); } @Subscribe 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 f5cf7e8ecc..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,12 +294,35 @@ 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)); + if (callback != null) + callback.result(new PumpEnactResult().success(false).comment(MainApp.sResources.getString(R.string.not_eng_mode_or_release))).run(); + return false; + } + // Compare with pump limits 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/androidaps/queue/commands/CommandSetProfile.java b/app/src/main/java/info/nightscout/androidaps/queue/commands/CommandSetProfile.java index fe236fa55f..8b2437fdc3 100644 --- a/app/src/main/java/info/nightscout/androidaps/queue/commands/CommandSetProfile.java +++ b/app/src/main/java/info/nightscout/androidaps/queue/commands/CommandSetProfile.java @@ -1,9 +1,14 @@ package info.nightscout.androidaps.queue.commands; import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; import info.nightscout.androidaps.data.Profile; import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.ProfileSwitch; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.interfaces.PluginBase; import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.SmsCommunicator.SmsCommunicatorPlugin; import info.nightscout.androidaps.queue.Callback; /** @@ -11,7 +16,7 @@ import info.nightscout.androidaps.queue.Callback; */ public class CommandSetProfile extends Command { - Profile profile; + private Profile profile; public CommandSetProfile(Profile profile, Callback callback) { commandType = CommandType.BASALPROFILE; @@ -24,6 +29,15 @@ public class CommandSetProfile extends Command { PumpEnactResult r = ConfigBuilderPlugin.getActivePump().setNewBasalProfile(profile); if (callback != null) callback.result(r).run(); + + // Send SMS notification if ProfileSwitch is comming from NS + ProfileSwitch profileSwitch = MainApp.getConfigBuilder().getProfileSwitchFromHistory(System.currentTimeMillis()); + if (r.enacted && profileSwitch.source == Source.NIGHTSCOUT) { + SmsCommunicatorPlugin smsCommunicatorPlugin = MainApp.getSpecificPlugin(SmsCommunicatorPlugin.class); + if (smsCommunicatorPlugin != null && smsCommunicatorPlugin.isEnabled(PluginBase.GENERAL)) { + smsCommunicatorPlugin.sendNotificationToAllNumbers(MainApp.sResources.getString(R.string.profile_set_ok)); + } + } } @Override diff --git a/app/src/main/java/info/nightscout/androidaps/receivers/KeepAliveReceiver.java b/app/src/main/java/info/nightscout/androidaps/receivers/KeepAliveReceiver.java index 3d3258c614..810bf124c9 100644 --- a/app/src/main/java/info/nightscout/androidaps/receivers/KeepAliveReceiver.java +++ b/app/src/main/java/info/nightscout/androidaps/receivers/KeepAliveReceiver.java @@ -52,7 +52,7 @@ public class KeepAliveReceiver extends BroadcastReceiver { private void checkPump() { final PumpInterface pump = ConfigBuilderPlugin.getActivePump(); final Profile profile = MainApp.getConfigBuilder().getProfile(); - if (pump != null && profile != null && profile.getBasal() != null) { + if (pump != null && profile != null) { Date lastConnection = pump.lastDataTime(); boolean isStatusOutdated = lastConnection.getTime() + STATUS_UPDATE_FREQUENCY < System.currentTimeMillis(); boolean isBasalOutdated = Math.abs(profile.getBasal() - pump.getBaseBasalRate()) > pump.getPumpDescription().basalStep; 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 915d8bfb27..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. @@ -76,7 +78,7 @@ public class LocalAlertUtils { final PumpInterface pump = ConfigBuilderPlugin.getActivePump(); final Profile profile = MainApp.getConfigBuilder().getProfile(); - if (pump != null && profile != null && profile.getBasal() != null) { + if (pump != null && profile != null) { Date lastConnection = pump.lastDataTime(); long earliestAlarmTime = lastConnection.getTime() + pumpUnreachableThreshold(); if (SP.getLong("nextPumpDisconnectedAlarm", 0l) < earliestAlarmTime) { 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/nsprofile_fragment.xml b/app/src/main/res/layout/nsprofile_fragment.xml index b1c88c41c1..e8a38195cb 100644 --- a/app/src/main/res/layout/nsprofile_fragment.xml +++ b/app/src/main/res/layout/nsprofile_fragment.xml @@ -12,7 +12,6 @@ \ No newline at end of file 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" />