diff --git a/app/build.gradle b/app/build.gradle index 736759f8dd..b83ba004c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -200,6 +200,8 @@ dependencies { compile "com.joanzapata.iconify:android-iconify-fontawesome:2.1.1" compile "com.google.android.gms:play-services-wearable:7.5.0" compile(name: "android-edittext-validator-v1.3.4-mod", ext: "aar") + compile(name: "sightparser-release", ext: "aar") + compile("com.google.android:flexbox:0.3.0") { exclude group: "com.android.support" } @@ -223,6 +225,7 @@ dependencies { testCompile "org.powermock:powermock-module-junit4-rule:${powermockVersion}" testCompile "org.powermock:powermock-module-junit4:${powermockVersion}" testCompile "joda-time:joda-time:2.9.4.2" + testCompile "com.google.truth:truth:0.39" androidTestCompile "org.mockito:mockito-core:2.7.22" androidTestCompile "com.google.dexmaker:dexmaker:${dexmakerVersion}" diff --git a/app/libs/sightparser-release.aar b/app/libs/sightparser-release.aar new file mode 100644 index 0000000000..fdf02e2abb Binary files /dev/null and b/app/libs/sightparser-release.aar differ diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index a05f1c6d0c..9103cbbe9d 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -54,6 +54,7 @@ import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPlugin; import info.nightscout.androidaps.plugins.PumpDanaRKorean.DanaRKoreanPlugin; import info.nightscout.androidaps.plugins.PumpDanaRS.DanaRSPlugin; import info.nightscout.androidaps.plugins.PumpDanaRv2.DanaRv2Plugin; +import info.nightscout.androidaps.plugins.PumpInsight.InsightPumpPlugin; import info.nightscout.androidaps.plugins.PumpMDI.MDIPlugin; import info.nightscout.androidaps.plugins.PumpVirtual.VirtualPumpPlugin; import info.nightscout.androidaps.plugins.SensitivityAAPS.SensitivityAAPSPlugin; @@ -129,6 +130,7 @@ public class MainApp extends Application { if (Config.DANAR) pluginsList.add(DanaRv2Plugin.getPlugin()); if (Config.DANAR) pluginsList.add(DanaRSPlugin.getPlugin()); pluginsList.add(CareportalPlugin.getPlugin()); + if (Config.DANAR) pluginsList.add(InsightPumpPlugin.getPlugin()); if (Config.MDI) pluginsList.add(MDIPlugin.getPlugin()); if (Config.VIRTUALPUMP) pluginsList.add(VirtualPumpPlugin.getPlugin()); if (Config.APS) pluginsList.add(LoopPlugin.getPlugin()); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/Cstatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/Cstatus.java new file mode 100644 index 0000000000..53c0cf798f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/Cstatus.java @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.PumpInsight; + +/** + * Created by jamorham on 25/01/2018. + * + * Async command status + * + */ +enum Cstatus { + UNKNOWN, + PENDING, + SUCCESS, + FAILURE, + TIMEOUT; + + boolean success() { + return this == SUCCESS; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpAsyncAdapter.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpAsyncAdapter.java new file mode 100644 index 0000000000..e28fd91707 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpAsyncAdapter.java @@ -0,0 +1,106 @@ +package info.nightscout.androidaps.plugins.PumpInsight; + +import android.os.PowerManager; + +import com.squareup.otto.Subscribe; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.plugins.PumpInsight.events.EventInsightPumpCallback; + +import static info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers.getWakeLock; +import static info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers.msSince; +import static info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers.releaseWakeLock; +import static info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers.tsl; + +/** + * Created by jamorham on 25/01/2018. + * + * Asynchronous adapter + * + */ + +public class InsightPumpAsyncAdapter { + + private final ConcurrentHashMap commandResults = new ConcurrentHashMap<>(); + + InsightPumpAsyncAdapter() { + MainApp.bus().register(this); + } + + // just log during debugging + private static void log(String msg) { + android.util.Log.e("INSIGHTPUMPASYNC", msg); + } + + @Subscribe + public void onStatusEvent(final EventInsightPumpCallback ev) { + log("Received callback event: " + ev.toString()); + commandResults.put(ev.request_uuid, ev); + } + + // poll command result + private Cstatus checkCommandResult(UUID uuid) { + if (uuid == null) return Cstatus.FAILURE; + if (commandResults.containsKey(uuid)) { + if (commandResults.get(uuid).success) { + return Cstatus.SUCCESS; + } else { + return Cstatus.FAILURE; + } + } else { + return Cstatus.PENDING; + } + } + + // blocking call to wait for result callback + Cstatus busyWaitForCommandResult(final UUID uuid, long wait_time) { + final PowerManager.WakeLock wl = getWakeLock("insight-wait-cmd", 60000); + try { + log("busy wait for command " + uuid); + if (uuid == null) return Cstatus.FAILURE; + final long start_time = tsl(); + Cstatus status = checkCommandResult(uuid); + while ((status == Cstatus.PENDING) && msSince(start_time) < wait_time) { + //log("command result waiting"); + try { + Thread.sleep(200); + } catch (InterruptedException e) { + log("Got interrupted exception! " + e); + } + status = checkCommandResult(uuid); + } + if (status == Cstatus.PENDING) { + return Cstatus.TIMEOUT; + } else { + return status; + } + } finally { + releaseWakeLock(wl); + } + } + + // commend field preparation for results + String getCommandComment(final UUID uuid) { + if (commandResults.containsKey(uuid)) { + if (commandResults.get(uuid).success) { + return "OK"; + } else { + return commandResults.get(uuid).message; + } + } else { + return "Unknown reference"; + } + } + + int getResponseID(UUID uuid) { + if (checkCommandResult(uuid) == Cstatus.SUCCESS) { + return commandResults.get(uuid).response_id; + } else { + return -2; // invalid + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpFragment.java new file mode 100644 index 0000000000..2d9cb0b202 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpFragment.java @@ -0,0 +1,113 @@ +package info.nightscout.androidaps.plugins.PumpInsight; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.crashlytics.android.Crashlytics; +import com.squareup.otto.Subscribe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.Common.SubscriberFragment; +import info.nightscout.androidaps.plugins.PumpInsight.events.EventInsightPumpUpdateGui; +import info.nightscout.androidaps.plugins.PumpInsight.utils.StatusItem; +import info.nightscout.androidaps.plugins.PumpInsight.utils.ui.StatusItemViewAdapter; + + +public class InsightPumpFragment extends SubscriberFragment { + private static final Logger log = LoggerFactory.getLogger(InsightPumpFragment.class); + private static final Handler sLoopHandler = new Handler(); + private static volatile boolean refresh = false; + private static volatile boolean pending = false; + StatusItemViewAdapter viewAdapter; + LinearLayout holder; + private final Runnable sRefreshLoop = new Runnable() { + @Override + public void run() { + pending = false; + updateGUI(); + if (refresh) { + scheduleRefresh(); + } + } + }; + + private synchronized void scheduleRefresh() { + if (!pending) { + pending = true; + sLoopHandler.postDelayed(sRefreshLoop, 30 * 1000L); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + try { + final View view = inflater.inflate(R.layout.insightpump_fragment, container, false); + holder = (LinearLayout) view.findViewById(R.id.insightholder); + viewAdapter = new StatusItemViewAdapter(getActivity(), holder); + + return view; + } catch (Exception e) { + Crashlytics.logException(e); + } + + return null; + } + + + @Override + public void setUserVisibleHint(boolean visible) { + super.setUserVisibleHint(visible); + if (visible) { + refresh = true; + pending = false; + updateGUI(); + scheduleRefresh(); + } else { + refresh = false; + //sLoopHandler.removeCallbacksAndMessages(null); + } + } + + + @Subscribe + public void onStatusEvent(final EventInsightPumpUpdateGui ev) { + updateGUI(); + } + + @Override + protected void updateGUI() { + final Activity activity = getActivity(); + if (activity != null && holder != null) + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + final InsightPumpPlugin insightPumpPlugin = InsightPumpPlugin.getPlugin(); + final List l = insightPumpPlugin.getStatusItems(refresh); + + holder.removeAllViews(); + + for (StatusItem row : l) { + viewAdapter.inflateStatus(row); + } + + } + }); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpPlugin.java new file mode 100644 index 0000000000..18ac83f261 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/InsightPumpPlugin.java @@ -0,0 +1,940 @@ +package info.nightscout.androidaps.plugins.PumpInsight; + +import android.os.Handler; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import info.nightscout.androidaps.BuildConfig; +import info.nightscout.androidaps.Config; +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.ExtendedBolus; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TemporaryBasal; +import info.nightscout.androidaps.interfaces.PluginBase; +import info.nightscout.androidaps.interfaces.PumpDescription; +import info.nightscout.androidaps.interfaces.PumpInterface; +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.PumpInsight.connector.AbsoluteTBRTaskRunner; +import info.nightscout.androidaps.plugins.PumpInsight.connector.CancelBolusTaskRunner; +import info.nightscout.androidaps.plugins.PumpInsight.connector.Connector; +import info.nightscout.androidaps.plugins.PumpInsight.events.EventInsightPumpCallback; +import info.nightscout.androidaps.plugins.PumpInsight.events.EventInsightPumpUpdateGui; +import info.nightscout.androidaps.plugins.PumpInsight.history.HistoryReceiver; +import info.nightscout.androidaps.plugins.PumpInsight.history.LiveHistory; +import info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers; +import info.nightscout.androidaps.plugins.PumpInsight.utils.StatusItem; +import info.nightscout.utils.DateUtil; +import info.nightscout.utils.NSUpload; +import info.nightscout.utils.SP; +import sugar.free.sightparser.applayer.descriptors.ActiveBolus; +import sugar.free.sightparser.applayer.descriptors.ActiveBolusType; +import sugar.free.sightparser.applayer.descriptors.PumpStatus; +import sugar.free.sightparser.applayer.messages.AppLayerMessage; +import sugar.free.sightparser.applayer.messages.remote_control.BolusMessage; +import sugar.free.sightparser.applayer.messages.remote_control.CancelTBRMessage; +import sugar.free.sightparser.applayer.messages.remote_control.ExtendedBolusMessage; +import sugar.free.sightparser.applayer.messages.remote_control.StandardBolusMessage; +import sugar.free.sightparser.handling.SingleMessageTaskRunner; +import sugar.free.sightparser.handling.TaskRunner; +import sugar.free.sightparser.handling.taskrunners.SetTBRTaskRunner; +import sugar.free.sightparser.handling.taskrunners.StatusTaskRunner; + +import static info.nightscout.androidaps.plugins.PumpInsight.history.PumpIdCache.getRecordUniqueID; +import static info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers.roundDouble; + + +/** + * Created by jamorham on 23/01/2018. + * + * Connects to SightRemote app service using SightParser library + * + * SightRemote and SightParser created by Tebbe Ubben + * + * Original proof of concept SightProxy by jamorham + * + */ + +public class InsightPumpPlugin implements PluginBase, PumpInterface { + + private static final long BUSY_WAIT_TIME = 20000; + static Integer batteryPercent = 0; + static Integer reservoirInUnits = 0; + static boolean initialized = false; + private static volatile boolean update_pending = false; + private static Logger log = LoggerFactory.getLogger(InsightPumpPlugin.class); + private static volatile InsightPumpPlugin plugin; + private final Handler handler = new Handler(); + private final InsightPumpAsyncAdapter async = new InsightPumpAsyncAdapter(); + private StatusTaskRunner.StatusResult statusResult; + private long statusResultTime = -1; + private Date lastDataTime = new Date(0); + private TaskRunner taskRunner; + private boolean fragmentEnabled = true; + private boolean fragmentVisible = true; + private boolean fauxTBRcancel = true; + private PumpDescription pumpDescription = new PumpDescription(); + private double basalRate = 0; + private Connector connector; + private final TaskRunner.ResultCallback statusResultHandler = new TaskRunner.ResultCallback() { + + @Override + public void onError(Exception e) { + log("Got error taskrunner: " + e); + android.util.Log.e("INSIGHTPUMP", "taskrunner stacktrace: ", e); + + if (e instanceof sugar.free.sightparser.error.DisconnectedError) { + if (Helpers.ratelimit("insight-reconnect", 2)) { + Connector.connectToPump(); + updateGui(); + } + } + } + + @Override + public synchronized void onResult(Object result) { + log("GOT STATUS RESULT!!!"); + statusResult = (StatusTaskRunner.StatusResult) result; + statusResultTime = Helpers.tsl(); + processStatusResult(); + updateGui(); + connector.requestHistoryReSync(); + connector.requestHistorySync(); + } + }; + + private InsightPumpPlugin() { + log("InsightPumpPlugin"); + pumpDescription.isBolusCapable = true; + pumpDescription.bolusStep = 0.05d; // specification says 0.05U up to 2U then 0.1U @ 2-5U 0.2U @ 10-20U 0.5U 10-20U (are these just UI restrictions?) + + pumpDescription.isExtendedBolusCapable = true; + pumpDescription.extendedBolusStep = 0.05d; // specification probably same as above + pumpDescription.extendedBolusDurationStep = 15; // 15 minutes up to 24 hours + pumpDescription.extendedBolusMaxDuration = 24 * 60; + + pumpDescription.isTempBasalCapable = true; + //pumpDescription.tempBasalStyle = PumpDescription.PERCENT | PumpDescription.ABSOLUTE; + pumpDescription.tempBasalStyle = PumpDescription.PERCENT; + + pumpDescription.maxTempPercent = 250; // 0-250% + pumpDescription.tempPercentStep = 10; + + pumpDescription.tempDurationStep = 15; // 15 minutes up to 24 hours + pumpDescription.tempMaxDuration = 24 * 60; + + pumpDescription.isSetBasalProfileCapable = false; // leave this for now + pumpDescription.basalStep = 0.01d; + pumpDescription.basalMinimumRate = 0.02d; + + pumpDescription.isRefillingCapable = true; + //pumpDescription.storesCarbInfo = false; // uncomment when PumpDescription updated to include this + + this.connector = Connector.get(); + this.connector.init(); + + log("back from init"); + } + + + public static InsightPumpPlugin getPlugin() { + if (plugin == null) { + createInstance(); + } + return plugin; + } + + private static synchronized void createInstance() { + if (plugin == null) { + log("creating instance"); + plugin = new InsightPumpPlugin(); + } + } + + // just log during debugging + private static void log(String msg) { + android.util.Log.e("INSIGHTPUMP", msg); + } + + private static void updateGui() { + update_pending = false; + MainApp.bus().post(new EventInsightPumpUpdateGui()); + } + + private static void pushCallbackEvent(EventInsightPumpCallback e) { + MainApp.bus().post(e); + } + + @Override + public String getFragmentClass() { + return InsightPumpFragment.class.getName(); + } + + @Override + public String getName() { + return MainApp.instance().getString(R.string.insightpump); + } + + @Override + public String getNameShort() { + String name = MainApp.instance().getString(R.string.insightpump_shortname); + if (!name.trim().isEmpty()) { + //only if translation exists + return name; + } + // use long name as fallback + return getName(); + } + + @Override + public boolean isEnabled(int type) { + return type == PUMP && fragmentEnabled; + } + + @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 true; + } + + @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 R.xml.pref_insightpump; + } + + @Override + public int getType() { + return PluginBase.PUMP; + } + + @Override + public boolean isFakingTempsByExtendedBoluses() { + return false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public boolean isSuspended() { + return !isPumpRunning(); + } + + @Override + public boolean isBusy() { + return false; + } + + @Override + public boolean isConnected() { + return Connector.get().isPumpConnected(); + } + + @Override + public boolean isConnecting() { + return Connector.get().isPumpConnecting(); + } + + @Override + public void connect(String reason) { + log("InsightPumpPlugin::connect()"); + try { + if (!connector.isPumpConnected()) { + if (Helpers.ratelimit("insight-connect-timer", 40)) { + log("Actually requesting a connect"); + connector.getServiceConnector().connect(); + } + } else { + log("Already connected"); + } + } catch (NullPointerException e) { + log("Could not sconnect - null pointer: " + e); + } + + // TODO review + if (!Config.NSCLIENT && !Config.G5UPLOADER) + NSUpload.uploadDeviceStatus(); + lastDataTime = new Date(); + } + + @Override + public void disconnect(String reason) { + log("InsightPumpPlugin::disconnect()"); + try { + if (!SP.getBoolean("insight_always_connected", false)) { + log("Requesting disconnect"); + connector.getServiceConnector().disconnect(); + } else { + log("Not disconnecting due to preference"); + } + } catch (NullPointerException e) { + log("Could not disconnect - null pointer: " + e); + } + } + + @Override + public void stopConnecting() { + log("InsightPumpPlugin::stopConnecting()"); + try { + if (isConnecting()) { + if (!SP.getBoolean("insight_always_connected", false)) { + log("Requesting disconnect"); + connector.getServiceConnector().disconnect(); + } else { + log("Not disconnecting due to preference"); + } + } else { + log("Not currently trying to connect so not stopping connection"); + } + } catch (NullPointerException e) { + log("Could not stop connecting - null pointer: " + e); + } + } + + @Override + public void getPumpStatus() { + + log("getPumpStatus"); + lastDataTime = new Date(); + if (Connector.get().isPumpConnected()) { + log("is connected.. requesting status"); + handler.postDelayed(new Runnable() { + @Override + public void run() { + taskRunner = new StatusTaskRunner(connector.getServiceConnector()); + taskRunner.fetch(statusResultHandler); + } + } + , 1000); + } else { + log("not connected.. not requesting status"); + } + } + + // TODO implement + @Override + public PumpEnactResult setNewBasalProfile(Profile profile) { + lastDataTime = new Date(); + // Do nothing here. we are using MainApp.getConfigBuilder().getActiveProfile().getProfile(); + PumpEnactResult result = new PumpEnactResult(); + result.enacted = false; + result.success = false; + Notification notification = new Notification(Notification.PROFILE_SET_OK, MainApp.sResources.getString(R.string.profile_set_ok), Notification.INFO, 60); + MainApp.bus().post(new EventNewNotification(notification)); + return result; + } + + @Override + public boolean isThisProfileSet(Profile profile) { + return true; + } + + @Override + public Date lastDataTime() { + return lastDataTime; + } + + @Override + public double getBaseBasalRate() { + return basalRate; + } + + public String getBaseBasalRateString() { + final DecimalFormat df = new DecimalFormat("#.##"); + return df.format(basalRate); + } + + @Override + public PumpEnactResult deliverTreatment(DetailedBolusInfo detailedBolusInfo) { + final PumpEnactResult result = new PumpEnactResult(); + result.bolusDelivered = detailedBolusInfo.insulin; + result.carbsDelivered = detailedBolusInfo.carbs; + result.enacted = result.bolusDelivered > 0 || result.carbsDelivered > 0; + result.comment = MainApp.instance().getString(R.string.virtualpump_resultok); + + result.percent = 100; + + // is there an insulin component to the treatment? + if (detailedBolusInfo.insulin > 0) { + final UUID cmd = deliverBolus((float) detailedBolusInfo.insulin); // actually request delivery + if (cmd == null) { + return pumpEnactFailure(); + } + final Cstatus cs = async.busyWaitForCommandResult(cmd, BUSY_WAIT_TIME); + result.success = cs.success(); + if (cs.success()) { + detailedBolusInfo.pumpId = getRecordUniqueID(async.getResponseID(cmd)); + } + } else { + result.success = true; // always true with carb only treatments + } + + if (result.success) { + log("Success!"); + + final EventOverviewBolusProgress bolusingEvent = EventOverviewBolusProgress.getInstance(); + bolusingEvent.status = String.format(MainApp.sResources.getString(R.string.bolusdelivered), detailedBolusInfo.insulin); + bolusingEvent.percent = 100; + MainApp.bus().post(bolusingEvent); + MainApp.getConfigBuilder().addToHistoryTreatment(detailedBolusInfo); + } else { + log.debug("Failure to deliver treatment"); + } + + if (Config.logPumpComm) + log.debug("Delivering treatment insulin: " + detailedBolusInfo.insulin + "U carbs: " + detailedBolusInfo.carbs + "g " + result); + + updateGui(); + connector.tryToGetPumpStatusAgain(); + + lastDataTime = new Date(); + connector.requestHistorySync(30000); + return result; + } + + @Override + public void stopBolusDelivering() { + final UUID cmd = aSyncTaskRunner(new CancelBolusTaskRunner(connector.getServiceConnector(), ActiveBolusType.STANDARD), "Cancel standard bolus"); + + if (cmd == null) { + return; + } + + final Cstatus cs = async.busyWaitForCommandResult(cmd, BUSY_WAIT_TIME); + log("Got command status: " + cs); + } + + // Temporary Basals + + @Override + public PumpEnactResult setTempBasalAbsolute(Double absoluteRate, Integer durationInMinutes, boolean enforceNew) { + absoluteRate = Helpers.roundDouble(absoluteRate, 3); + log("Set TBR absolute: " + absoluteRate); + + final AbsoluteTBRTaskRunner task = new AbsoluteTBRTaskRunner(connector.getServiceConnector(), absoluteRate, durationInMinutes); + final UUID cmd = aSyncTaskRunner(task, "Set TBR abs: " + absoluteRate + " " + durationInMinutes + "m"); + + if (cmd == null) { + return pumpEnactFailure(); + } + + Cstatus cs = async.busyWaitForCommandResult(cmd, BUSY_WAIT_TIME); + log("Got command status: " + cs); + + PumpEnactResult pumpEnactResult = new PumpEnactResult().enacted(true).isPercent(false).duration(durationInMinutes); + pumpEnactResult.absolute = absoluteRate; // TODO get converted value? + pumpEnactResult.success = cs.success(); + pumpEnactResult.isTempCancel = false; // do we test this here? + pumpEnactResult.comment = async.getCommandComment(cmd); + + if (pumpEnactResult.success) { + // create log entry + final TemporaryBasal tempBasal = new TemporaryBasal(); + tempBasal.date = System.currentTimeMillis(); + tempBasal.isAbsolute = true; + tempBasal.absoluteRate = task.getCalculatedAbsolute(); // is this the correct figure to use? + tempBasal.durationInMinutes = durationInMinutes; + tempBasal.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(tempBasal); + } + + if (Config.logPumpComm) + log.debug("Setting temp basal absolute: " + pumpEnactResult.success); + + lastDataTime = new Date(); + + updateGui(); + + connector.requestHistorySync(5000); + connector.tryToGetPumpStatusAgain(); + + return pumpEnactResult; + } + + + @Override + public PumpEnactResult setTempBasalPercent(Integer percent, Integer durationInMinutes, boolean enforceNew) { + log("Set TBR %"); + final UUID cmd = aSyncTaskRunner(new SetTBRTaskRunner(connector.getServiceConnector(), percent, durationInMinutes), "Set TBR " + percent + "%" + " " + durationInMinutes + "m"); + + if (cmd == null) { + return pumpEnactFailure(); + } + + Cstatus cs = async.busyWaitForCommandResult(cmd, BUSY_WAIT_TIME); + log("Got command status: " + cs); + + PumpEnactResult pumpEnactResult = new PumpEnactResult().enacted(true).isPercent(true).duration(durationInMinutes); + pumpEnactResult.percent = percent; + pumpEnactResult.success = cs.success(); + pumpEnactResult.isTempCancel = percent == 100; // 100% temp basal is a cancellation + pumpEnactResult.comment = async.getCommandComment(cmd); + + if (pumpEnactResult.success) { + // create log entry + final TemporaryBasal tempBasal = new TemporaryBasal(); + tempBasal.date = System.currentTimeMillis(); + tempBasal.isAbsolute = false; + tempBasal.percentRate = percent; + tempBasal.durationInMinutes = durationInMinutes; + tempBasal.source = Source.USER; // TODO check this is correct + MainApp.getConfigBuilder().addToHistoryTempBasal(tempBasal); + } + + updateGui(); + + if (Config.logPumpComm) + log.debug("Set temp basal " + percent + "% for " + durationInMinutes + "m"); + + connector.requestHistorySync(5000); + connector.tryToGetPumpStatusAgain(); + + return pumpEnactResult; + } + + + @Override + public PumpEnactResult cancelTempBasal(boolean enforceNew) { + log("Cancel TBR"); + + + fauxTBRcancel = !SP.getBoolean("insight_real_tbr_cancel", false); + + final UUID cmd; + + if (fauxTBRcancel) { + final int faux_percent = 90; + final int faux_duration = 15; + cmd = aSyncTaskRunner(new SetTBRTaskRunner(connector.getServiceConnector(), faux_percent, 15), "Faux Cancel TBR - setting " + faux_percent + "%" + " " + faux_duration + "m"); + } else { + cmd = aSyncSingleCommand(new CancelTBRMessage(), "Cancel Temp Basal"); + } + if (cmd == null) { + return pumpEnactFailure(); + } + + // TODO isn't conditional on one apparently being in progress only the history change + boolean enacted = false; + final Cstatus cs = async.busyWaitForCommandResult(cmd, BUSY_WAIT_TIME); + + if (MainApp.getConfigBuilder().isTempBasalInProgress()) { + enacted = true; + TemporaryBasal tempStop = new TemporaryBasal(System.currentTimeMillis()); + tempStop.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryTempBasal(tempStop); + } + lastDataTime = new Date(); + updateGui(); + if (Config.logPumpComm) + log.debug("Canceling temp basal: "); // TODO get more info + + connector.requestHistorySync(5000); + connector.tryToGetPumpStatusAgain(); + + return new PumpEnactResult().success(cs.success()).enacted(true).isTempCancel(true); + } + + + // Extended Boluses + + @Override + public PumpEnactResult setExtendedBolus(Double insulin, Integer durationInMinutes) { + log("Set Extended bolus " + insulin + " " + durationInMinutes); + ExtendedBolusMessage extendedBolusMessage = new ExtendedBolusMessage(); + extendedBolusMessage.setAmount((float) ((double) insulin)); + extendedBolusMessage.setDuration((short) ((int) durationInMinutes)); + final UUID cmd = aSyncSingleCommand(extendedBolusMessage, "Extended bolus U" + insulin + " mins:" + durationInMinutes); + if (cmd == null) { + return pumpEnactFailure(); + } + + final Cstatus cs = async.busyWaitForCommandResult(cmd, BUSY_WAIT_TIME); + log("Got command status: " + cs); + + PumpEnactResult pumpEnactResult = new PumpEnactResult().enacted(true).bolusDelivered(insulin).duration(durationInMinutes); + pumpEnactResult.success = cs.success(); + pumpEnactResult.comment = async.getCommandComment(cmd); + + if (pumpEnactResult.success) { + // create log entry + final ExtendedBolus extendedBolus = new ExtendedBolus(); + extendedBolus.date = System.currentTimeMillis(); + extendedBolus.insulin = insulin; + extendedBolus.durationInMinutes = durationInMinutes; + extendedBolus.source = Source.USER; + extendedBolus.pumpId = getRecordUniqueID(async.getResponseID(cmd)); + MainApp.getConfigBuilder().addToHistoryExtendedBolus(extendedBolus); + } + + if (Config.logPumpComm) + log.debug("Setting extended bolus: " + insulin + " mins:" + durationInMinutes + " " + pumpEnactResult.comment); + + updateGui(); + + connector.requestHistorySync(30000); + connector.tryToGetPumpStatusAgain(); + + return pumpEnactResult; + } + + @Override + public PumpEnactResult cancelExtendedBolus() { + + log("Cancel Extended bolus"); + + // TODO note always sends cancel to pump but only changes history if present + + final UUID cmd = aSyncTaskRunner(new CancelBolusTaskRunner(connector.getServiceConnector(), ActiveBolusType.EXTENDED), "Cancel extended bolus"); + + if (cmd == null) { + return pumpEnactFailure(); + } + + final Cstatus cs = async.busyWaitForCommandResult(cmd, BUSY_WAIT_TIME); + + if (MainApp.getConfigBuilder().isInHistoryExtendedBoluslInProgress()) { + ExtendedBolus exStop = new ExtendedBolus(System.currentTimeMillis()); + exStop.source = Source.USER; + MainApp.getConfigBuilder().addToHistoryExtendedBolus(exStop); + } + + if (Config.logPumpComm) + log.debug("Cancel extended bolus:"); + + updateGui(); + + connector.requestHistorySync(5000); + connector.tryToGetPumpStatusAgain(); + + return new PumpEnactResult().success(cs.success()).enacted(true); + } + + + private synchronized UUID deliverBolus(float bolusValue) { + log("DeliverBolus: " + bolusValue); + + if (bolusValue == 0) return null; + if (bolusValue < 0) return null; + + // TODO check limits here or they already occur via a previous constraint interface? + + final StandardBolusMessage message = new StandardBolusMessage(); + message.setAmount(bolusValue); + + return aSyncSingleCommand(message, "Deliver Bolus " + bolusValue); + } + + @Override + public JSONObject getJSONStatus() { + + if (Helpers.msSince(connector.getLastContactTime()) > (60 * 60 * 1000)) { + log("getJSONStatus not returning as data likely stale"); + return null; + } + + final JSONObject pump = new JSONObject(); + final JSONObject battery = new JSONObject(); + final JSONObject status = new JSONObject(); + final JSONObject extended = new JSONObject(); + try { + battery.put("percent", batteryPercent); + status.put("status", isSuspended() ? "suspended" : "normal"); + status.put("timestamp", DateUtil.toISOString(connector.getLastContactTime())); + extended.put("Version", BuildConfig.VERSION_NAME + "-" + BuildConfig.BUILDVERSION); + try { + extended.put("ActiveProfile", MainApp.getConfigBuilder().getProfileName()); + } catch (Exception e) { + } + TemporaryBasal tb = MainApp.getConfigBuilder().getTempBasalFromHistory(System.currentTimeMillis()); + if (tb != null) { + extended.put("TempBasalAbsoluteRate", tb.tempBasalConvertedToAbsolute(System.currentTimeMillis())); + extended.put("TempBasalStart", DateUtil.dateAndTimeString(tb.date)); + extended.put("TempBasalRemaining", tb.getPlannedRemainingMinutes()); + } + ExtendedBolus eb = MainApp.getConfigBuilder().getExtendedBolusFromHistory(System.currentTimeMillis()); + if (eb != null) { + extended.put("ExtendedBolusAbsoluteRate", eb.absoluteRate()); + extended.put("ExtendedBolusStart", DateUtil.dateAndTimeString(eb.date)); + extended.put("ExtendedBolusRemaining", eb.getPlannedRemainingMinutes()); + } + status.put("timestamp", DateUtil.toISOString(new Date())); + + pump.put("battery", battery); + pump.put("status", status); + pump.put("extended", extended); + pump.put("reservoir", reservoirInUnits); + pump.put("clock", DateUtil.toISOString(new Date())); + } catch (JSONException e) { + log.error("Unhandled exception", e); + } + return pump; + } + + @Override + public String deviceID() { + return "InsightPump"; + } + + @Override + public PumpDescription getPumpDescription() { + return pumpDescription; + } + + @Override + public String shortStatus(boolean veryShort) { + String msg = gs(R.string.insightpump_shortname) + " Batt: " + batteryPercent + " Reserv: " + reservoirInUnits + " Basal: " + basalRate; + if (LiveHistory.getStatus().length() > 0) { + msg += LiveHistory.getStatus(); + } + return msg; + } + + private void processStatusResult() { + if (statusResult != null) { + batteryPercent = statusResult.getBatteryAmountMessage().getBatteryAmount(); + reservoirInUnits = (int) statusResult.getCartridgeAmountMessage().getCartridgeAmount(); + basalRate = roundDouble(statusResult.getCurrentBasalMessage().getCurrentBasalAmount(), 2); + initialized = true; // basic communication test + } + } + + private String gs(int id) { + return MainApp.instance().getString(id); + } + + private boolean isPumpRunning() { + if (statusResult == null) return true; // assume running if we have no information + return statusResult.getPumpStatusMessage().getPumpStatus() == PumpStatus.STARTED; + } + + List getStatusItems(boolean refresh) { + final List l = new ArrayList<>(); + + // Todo last contact time + + l.add(new StatusItem(gs(R.string.status_no_colon), connector.getLastStatusMessage())); + l.add(new StatusItem(gs(R.string.changed), connector.getNiceLastStatusTime())); + + boolean pumpRunning; + // also check time since received + if (statusResult != null) { + + pumpRunning = isPumpRunning(); + if (pumpRunning) { + l.add(new StatusItem(gs(R.string.pump_basebasalrate_label), getBaseBasalRateString() + "U")); + } else { + l.add(new StatusItem(gs(R.string.combo_warning), gs(R.string.pump_stopped_uppercase), StatusItem.Highlight.CRITICAL)); + } + } + + final long offset_ms = Helpers.msSince(statusResultTime); + final long offset_minutes = offset_ms / 60000; + + if (statusResult != null) { + l.add(new StatusItem(gs(R.string.status_updated), Helpers.niceTimeScalar(Helpers.msSince(statusResultTime)) + " " + gs(R.string.ago))); + l.add(new StatusItem(gs(R.string.pump_battery_label), batteryPercent + "%", batteryPercent < 100 ? + (batteryPercent < 90 ? + (batteryPercent < 70 ? + (StatusItem.Highlight.BAD) : StatusItem.Highlight.NOTICE) : StatusItem.Highlight.NORMAL) : StatusItem.Highlight.GOOD)); + l.add(new StatusItem(gs(R.string.pump_reservoir_label), reservoirInUnits + "U")); + + if (statusResult.getCurrentTBRMessage().getPercentage() != 100) { + l.add(new StatusItem(gs(R.string.insight_active_tbr), statusResult.getCurrentTBRMessage().getPercentage() + "% " + gs(R.string.with) + " " + + Helpers.qs(statusResult.getCurrentTBRMessage().getLeftoverTime() - offset_minutes, 0) + + " " + gs(R.string.insight_min_left), StatusItem.Highlight.NOTICE)); + } + + } + + if (MainApp.getConfigBuilder().isTempBasalInProgress()) { + try { + l.add(new StatusItem(gs(R.string.pump_tempbasal_label), MainApp.getConfigBuilder().getTempBasalFromHistory(System.currentTimeMillis()).toStringFull())); + } catch (NullPointerException e) { + // + } + } + + if (statusResult != null) { + statusActiveBolus(statusResult.getActiveBolusesMessage().getBolus1(), offset_minutes, l); + statusActiveBolus(statusResult.getActiveBolusesMessage().getBolus2(), offset_minutes, l); + statusActiveBolus(statusResult.getActiveBolusesMessage().getBolus3(), offset_minutes, l); + } + + if (MainApp.getConfigBuilder().isInHistoryExtendedBoluslInProgress()) { + try { + + l.add(new StatusItem(gs(R.string.virtualpump_extendedbolus_label), MainApp.getConfigBuilder().getExtendedBolusFromHistory(System.currentTimeMillis()).toString())); + } catch (NullPointerException e) { + // + } + } + + l.add(new StatusItem(gs(R.string.log_book), HistoryReceiver.getStatusString())); + + if (LiveHistory.getStatus().length() > 0) { + l.add(new StatusItem(gs(R.string.insight_last_completed_action), LiveHistory.getStatus())); + } + + if (Helpers.ratelimit("insight-status-ui-refresh", 10)) { + connector.tryToGetPumpStatusAgain(); + } + connector.requestHistorySync(); + if (refresh) scheduleGUIUpdate(); + + return l; + } + + private synchronized void scheduleGUIUpdate() { + if (!update_pending && connector.uiFresh()) { + update_pending = true; + Helpers.runOnUiThreadDelayed(new Runnable() { + @Override + public void run() { + updateGui(); + } + }, 500); + } + } + + private void statusActiveBolus(ActiveBolus activeBolus, long offset_mins, List l) { + if (activeBolus == null) return; + switch (activeBolus.getBolusType()) { + + case STANDARD: + l.add(new StatusItem(activeBolus.getBolusType() + " " + gs(R.string.bolus), activeBolus.getInitialAmount() + "U", StatusItem.Highlight.NOTICE)); + break; + case EXTENDED: + l.add(new StatusItem(activeBolus.getBolusType() + " " + gs(R.string.bolus), activeBolus.getInitialAmount() + "U " + gs(R.string.insight_total_with) + " " + + activeBolus.getLeftoverAmount() + "U " + gs(R.string.insight_remaining_over) + " " + (activeBolus.getDuration() - offset_mins) + " " + gs(R.string.insight_min), StatusItem.Highlight.NOTICE)); + break; + case MULTIWAVE: + l.add(new StatusItem(activeBolus.getBolusType() + " " + gs(R.string.bolus), activeBolus.getInitialAmount() + "U " + gs(R.string.insight_upfront_with) + " " + + activeBolus.getLeftoverAmount() + "U " + gs(R.string.insight_remaining_over) + " " + (activeBolus.getDuration() - offset_mins) + " " + gs(R.string.insight_min), StatusItem.Highlight.NOTICE)); + + break; + default: + log("ERROR: unknown bolus type! " + activeBolus.getBolusType()); + } + } + + // Utility + + private synchronized UUID aSyncSingleCommand(final AppLayerMessage msg, final String name) { + // if (!isConnected()) return false; + //if (isBusy()) return false; + log("asyncSinglecommand called: " + name); + final EventInsightPumpCallback event = new EventInsightPumpCallback(); + new Thread() { + @Override + public void run() { + log("asyncSingleCommand thread"); + final SingleMessageTaskRunner singleMessageTaskRunner = new SingleMessageTaskRunner(connector.getServiceConnector(), msg); + try { + singleMessageTaskRunner.fetch(new TaskRunner.ResultCallback() { + @Override + public void onResult(Object o) { + log(name + " success"); + if (o instanceof BolusMessage) { + event.response_id = ((BolusMessage)o).getBolusId(); + } + event.success = true; + pushCallbackEvent(event); + } + + @Override + public void onError(Exception e) { + log(name + " error"); + event.message = e.getMessage(); + pushCallbackEvent(event); + } + }); + + } catch (Exception e) { + log("EXCEPTION" + e.toString()); + } + } + }.start(); + return event.request_uuid; + } + + private synchronized UUID aSyncTaskRunner(final TaskRunner task, final String name) { + // if (!isConnected()) return false; + //if (isBusy()) return false; + log("asyncTaskRunner called: " + name); + final EventInsightPumpCallback event = new EventInsightPumpCallback(); + new Thread() { + @Override + public void run() { + log("asyncTaskRunner thread"); + try { + task.fetch(new TaskRunner.ResultCallback() { + @Override + public void onResult(Object o) { + log(name + " success"); + event.success = true; + pushCallbackEvent(event); + } + + @Override + public void onError(Exception e) { + log(name + " error"); + event.message = e.getMessage(); + pushCallbackEvent(event); + } + }); + + } catch (Exception e) { + log("EXCEPTION" + e.toString()); + } + } + }.start(); + return event.request_uuid; + } + + + private PumpEnactResult pumpEnactFailure() { + return new PumpEnactResult().success(false).enacted(false); + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/AbsoluteTBRTaskRunner.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/AbsoluteTBRTaskRunner.java new file mode 100644 index 0000000000..923ed84dc8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/AbsoluteTBRTaskRunner.java @@ -0,0 +1,60 @@ +package info.nightscout.androidaps.plugins.PumpInsight.connector; + +import info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers; +import sugar.free.sightparser.applayer.messages.AppLayerMessage; +import sugar.free.sightparser.applayer.messages.remote_control.ChangeTBRMessage; +import sugar.free.sightparser.applayer.messages.remote_control.SetTBRMessage; +import sugar.free.sightparser.applayer.messages.status.CurrentBasalMessage; +import sugar.free.sightparser.applayer.messages.status.CurrentTBRMessage; +import sugar.free.sightparser.handling.SightServiceConnector; +import sugar.free.sightparser.handling.TaskRunner; + +// by Tebbe Ubben + +public class AbsoluteTBRTaskRunner extends TaskRunner { + + private double absolute; + private int amount; + private int duration; + private int calculated_percentage; + private double calculated_absolute; + + public AbsoluteTBRTaskRunner(SightServiceConnector serviceConnector, double absolute, int duration) { + super(serviceConnector); + if (absolute < 0) absolute = 0; + this.absolute = absolute; + this.duration = duration; + } + + public int getCalculatedPercentage() { + return calculated_percentage; + } + + public double getCalculatedAbsolute() { + return calculated_absolute; + } + + @Override + protected AppLayerMessage run(AppLayerMessage message) throws Exception { + if (message == null) return new CurrentBasalMessage(); + else if (message instanceof CurrentBasalMessage) { + float currentBasal = ((CurrentBasalMessage) message).getCurrentBasalAmount(); + amount = (int) (100d / currentBasal * absolute); + amount = ((int) amount / 10) * 10; + if (amount > 250) amount = 250; + calculated_percentage = amount; + calculated_absolute = Helpers.roundDouble(calculated_percentage * (double) currentBasal / 100d, 3); + Connector.log("Asked: " + absolute + " current: " + currentBasal + " calculated as: " + amount + "%" + " = " + calculated_absolute); + return new CurrentTBRMessage(); + } else if (message instanceof CurrentTBRMessage) { + SetTBRMessage setTBRMessage; + if (((CurrentTBRMessage) message).getPercentage() == 100) + setTBRMessage = new SetTBRMessage(); + else setTBRMessage = new ChangeTBRMessage(); + setTBRMessage.setAmount((short) amount); + setTBRMessage.setDuration((short) duration); + return setTBRMessage; + } else if (message instanceof SetTBRMessage) finish(amount); + return null; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/CancelBolusTaskRunner.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/CancelBolusTaskRunner.java new file mode 100644 index 0000000000..f350b80851 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/CancelBolusTaskRunner.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.PumpInsight.connector; + +import sugar.free.sightparser.applayer.messages.AppLayerMessage; +import sugar.free.sightparser.applayer.descriptors.ActiveBolusType; +import sugar.free.sightparser.applayer.messages.remote_control.CancelBolusMessage; +import sugar.free.sightparser.applayer.messages.status.ActiveBolusesMessage; +import sugar.free.sightparser.handling.SightServiceConnector; +import sugar.free.sightparser.handling.TaskRunner; + +// by Tebbe Ubben + +public class CancelBolusTaskRunner extends TaskRunner { + + private ActiveBolusType bolusType; + + public CancelBolusTaskRunner(SightServiceConnector serviceConnector, ActiveBolusType bolusType) { + super(serviceConnector); + this.bolusType = bolusType; + } + + @Override + protected AppLayerMessage run(AppLayerMessage message) throws Exception { + if (message == null) return new ActiveBolusesMessage(); + else if (message instanceof ActiveBolusesMessage) { + ActiveBolusesMessage bolusesMessage = (ActiveBolusesMessage) message; + CancelBolusMessage cancelBolusMessage = new CancelBolusMessage(); + if (bolusesMessage.getBolus1().getBolusType() == bolusType) + cancelBolusMessage.setBolusId(bolusesMessage.getBolus1().getBolusID()); + else if (bolusesMessage.getBolus2().getBolusType() == bolusType) + cancelBolusMessage.setBolusId(bolusesMessage.getBolus2().getBolusID()); + else if (bolusesMessage.getBolus3().getBolusType() == bolusType) + cancelBolusMessage.setBolusId(bolusesMessage.getBolus3().getBolusID()); + else finish(null); + return cancelBolusMessage; + } else if (message instanceof CancelBolusMessage) finish(null); + return null; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/Connector.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/Connector.java new file mode 100644 index 0000000000..fea7895a27 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/connector/Connector.java @@ -0,0 +1,357 @@ +package info.nightscout.androidaps.plugins.PumpInsight.connector; + +import android.content.Intent; +import android.os.PowerManager; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.PumpInsight.events.EventInsightPumpUpdateGui; +import info.nightscout.androidaps.plugins.PumpInsight.history.HistoryReceiver; +import info.nightscout.androidaps.plugins.PumpInsight.history.LiveHistory; +import info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers; +import sugar.free.sightparser.handling.ServiceConnectionCallback; +import sugar.free.sightparser.handling.SightServiceConnector; +import sugar.free.sightparser.handling.StatusCallback; +import sugar.free.sightparser.pipeline.Status; + +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_START_RESYNC; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_START_SYNC; +import static sugar.free.sightparser.handling.SightService.COMPATIBILITY_VERSION; + +/** + * Created by jamorham on 23/01/2018. + * + * Connects to SightRemote app service using SightParser library + * + * SightRemote and SightParser created by Tebbe Ubben + * + * Original proof of concept SightProxy by jamorham + * + */ + +public class Connector { + + private static final String TAG = "InsightConnector"; + private static final String COMPANION_APP_PACKAGE = "sugar.free.sightremote"; + private final static long FRESH_MS = 70000; + private static volatile Connector instance; + private static volatile HistoryReceiver historyReceiver; + private volatile SightServiceConnector serviceConnector; + private volatile Status lastStatus = null; + private String compatabilityMessage = null; + private volatile long lastStatusTime = -1; + private volatile long lastContactTime = -1; + private boolean companionAppInstalled = false; + private int serviceReconnects = 0; + private StatusCallback statusCallback = new StatusCallback() { + @Override + public synchronized void onStatusChange(Status status) { + + log("Status change: " + status); + lastStatus = status; + lastStatusTime = Helpers.tsl(); + if (status == Status.CONNECTED) { + lastContactTime = lastStatusTime; + } + + MainApp.bus().post(new EventInsightPumpUpdateGui()); + } + + }; + private ServiceConnectionCallback connectionCallback = new ServiceConnectionCallback() { + + @Override + public synchronized void onServiceConnected() { + log("On service connected"); + try { + final String remoteVersion = serviceConnector.getRemoteVersion(); + if (remoteVersion.equals(COMPATIBILITY_VERSION)) { + serviceConnector.connect(); + } else { + log("PROTOCOL VERSION MISMATCH! local: " + COMPATIBILITY_VERSION + " remote: " + remoteVersion); + statusCallback.onStatusChange(Status.INCOMPATIBLE); + compatabilityMessage = gs(R.string.insight_incompatible_compantion_app_we_need_version) + " " + getLocalVersion(); + serviceConnector.disconnectFromService(); + + } + } catch (NullPointerException e) { + log("ERROR: null pointer when trying to connect to pump"); + } + statusCallback.onStatusChange(safeGetStatus()); + } + + @Override + public synchronized void onServiceDisconnected() { + log("Disconnected from service"); + if (Helpers.ratelimit("insight-automatic-reconnect", 30)) { + log("Scheduling automatic service reconnection"); + Helpers.runOnUiThreadDelayed(new Runnable() { + @Override + public void run() { + init(); + } + }, 20000); + } + } + }; + + private Connector() { + initializeHistoryReceiver(); + } + + public static Connector get() { + if (instance == null) { + init_instance(); + } + return instance; + } + + private synchronized static void init_instance() { + if (instance == null) { + instance = new Connector(); + } + } + + private static boolean isCompanionAppInstalled() { + return Helpers.checkPackageExists(MainApp.instance(), TAG, COMPANION_APP_PACKAGE); + } + + public static void connectToPump() { + log("Attempting to connect to pump"); + get().getServiceConnector().connect(); + } + + static void log(String msg) { + android.util.Log.e("INSIGHTPUMP", msg); + } + + static String getLocalVersion() { + return COMPATIBILITY_VERSION; + } + + private static String statusToString(Status status) { + switch (status) { + + case EXCHANGING_KEYS: + return gs(R.string.connecting).toUpperCase(); + case WAITING_FOR_CODE_CONFIRMATION: + return gs(R.string.insight_waiting_for_code).toUpperCase(); + case CODE_REJECTED: + return gs(R.string.insight_code_rejected).toUpperCase(); + case APP_BINDING: + return gs(R.string.insight_app_binding).toUpperCase(); + case CONNECTING: + return gs(R.string.connecting).toUpperCase(); + case CONNECTED: + return gs(R.string.connected).toUpperCase(); + case DISCONNECTED: + return gs(R.string.disconnected).toUpperCase(); + case NOT_AUTHORIZED: + return gs(R.string.insight_not_authorized).toUpperCase(); + case INCOMPATIBLE: + return gs(R.string.insight_incompatible).toUpperCase(); + + default: + return status.toString(); + } + } + + private static String gs(int id) { + return MainApp.instance().getString(id); + } + + @SuppressWarnings("AccessStaticViaInstance") + private synchronized void initializeHistoryReceiver() { + if (historyReceiver == null) { + historyReceiver = new HistoryReceiver(); + } + historyReceiver.registerHistoryReceiver(); + } + + public synchronized void init() { + log("Connector::init()"); + if (serviceConnector == null) { + companionAppInstalled = isCompanionAppInstalled(); + if (companionAppInstalled) { + serviceConnector = new SightServiceConnector(MainApp.instance()); + serviceConnector.removeStatusCallback(statusCallback); + serviceConnector.addStatusCallback(statusCallback); + serviceConnector.setConnectionCallback(connectionCallback); + serviceConnector.connectToService(); + log("Trying to connect"); + } else { + log("Not trying init due to missing companion app"); + } + } else { + if (!serviceConnector.isConnectedToService()) { + if (serviceReconnects > 0) { + serviceConnector = null; + init(); + } else { + log("Trying to reconnect to service (" + serviceReconnects + ")"); + serviceConnector.connectToService(); + serviceReconnects++; + } + } else { + serviceReconnects = 0; // everything ok + } + } + } + + public SightServiceConnector getServiceConnector() { + init(); + return serviceConnector; + } + + public String getCurrent() { + init(); + return safeGetStatus().toString(); + } + + public Status safeGetStatus() { + try { + if (isConnected()) return serviceConnector.getStatus(); + return Status.DISCONNECTED; + } catch (IllegalArgumentException e) { + return Status.INCOMPATIBLE; + } + } + + public Status getLastStatus() { + return lastStatus; + } + + public boolean isConnected() { + return serviceConnector != null && serviceConnector.isConnectedToService(); + } + + public boolean isPumpConnected() { + return isConnected() && getLastStatus() == Status.CONNECTED; + } + + public boolean isPumpConnecting() { + return isConnected() && getLastStatus() == Status.CONNECTING; + } + + public long getLastContactTime() { + return lastContactTime; + } + + public String getLastStatusMessage() { + + if (!companionAppInstalled) { + return gs(R.string.insight_companion_app_not_installed); + } + + if (!isConnected()) { + log("Not connected to companion"); + if (Helpers.ratelimit("insight-app-not-connected", 5)) { + init(); + } + + if ((lastStatus == null) || (lastStatus != Status.INCOMPATIBLE)) { + if (compatabilityMessage != null) { + // if disconnected but previous state was incompatible + return compatabilityMessage; + } else { + return gs(R.string.insight_not_connected_to_companion_app); + } + } + } + + if (lastStatus == null) { + return gs(R.string.insight_unknown); + } + + switch (lastStatus) { + case CONNECTED: + if (Helpers.msSince(lastStatusTime) > (60 * 10 * 1000)) { + tryToGetPumpStatusAgain(); + } + break; + case INCOMPATIBLE: + return statusToString(lastStatus) + " " + gs(R.string.insight_needs) + " " + getLocalVersion(); + } + return statusToString(lastStatus); + } + + public String getNiceLastStatusTime() { + if (lastStatusTime < 1) { + return gs(R.string.insight_startup_uppercase); + } else { + return Helpers.niceTimeScalar(Helpers.msSince(lastStatusTime)) + " " + gs(R.string.ago); + } + } + + public boolean uiFresh() { + // todo check other changes + + if (Helpers.msSince(lastStatusTime) < FRESH_MS) { + return true; + } + if (Helpers.msSince(LiveHistory.getStatusTime()) < FRESH_MS) { + return true; + } + return false; + } + + @SuppressWarnings("AccessStaticViaInstance") + public void tryToGetPumpStatusAgain() { + if (Helpers.ratelimit("insight-retry-status-request", 5)) { + try { + MainApp.getConfigBuilder().getCommandQueue().readStatus("Insight. Status missing", null); + } catch (NullPointerException e) { + // + } + } + } + + public void requestHistorySync() { + requestHistorySync(0); + } + + public void requestHistoryReSync() { + requestHistoryReSync(0); + } + + public void requestHistorySync(long delay) { + if (Helpers.ratelimit("insight-history-sync-request", 10)) { + final Intent intent = new Intent(ACTION_START_SYNC); + sendBroadcastToCompanion(intent, delay); + } + } + + public void requestHistoryReSync(long delay) { + if (Helpers.ratelimit("insight-history-resync-request", 300)) { + final Intent intent = new Intent(ACTION_START_RESYNC); + sendBroadcastToCompanion(intent, delay); + } + } + + private void sendBroadcastToCompanion(final Intent intent, final long delay) { + new Thread(new Runnable() { + @Override + public void run() { + final PowerManager.WakeLock wl = Helpers.getWakeLock("insight-companion-delay", 60000); + intent.setPackage(COMPANION_APP_PACKAGE); + intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + try { + if (delay > 0) { + + Thread.sleep(delay); + } + } catch (InterruptedException e) { + // + } finally { + Helpers.releaseWakeLock(wl); + } + MainApp.instance().sendBroadcast(intent); + } + }).start(); + } + + public boolean lastStatusRecent() { + return true; // TODO evaluate whether current + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/events/EventInsightPumpCallback.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/events/EventInsightPumpCallback.java new file mode 100644 index 0000000000..baa13922f5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/events/EventInsightPumpCallback.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.PumpInsight.events; + +import java.util.UUID; + +import info.nightscout.androidaps.events.Event; + +/** + * Created by jamorham on 23/01/2018. + */ +public class EventInsightPumpCallback extends Event { + + public UUID request_uuid; + public boolean success = false; + public String message = null; + public int response_id = -1; + + public EventInsightPumpCallback() { + request_uuid = UUID.randomUUID(); + } + + @Override + public String toString() { + return "Event: " + request_uuid + " success: " + success + " msg: " + message; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/events/EventInsightPumpUpdateGui.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/events/EventInsightPumpUpdateGui.java new file mode 100644 index 0000000000..79e975ac31 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/events/EventInsightPumpUpdateGui.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.PumpInsight.events; + +import info.nightscout.androidaps.events.EventUpdateGui; + +/** + * Created by jamorham on 23/01/2018. + */ +public class EventInsightPumpUpdateGui extends EventUpdateGui { +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryIntentAdapter.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryIntentAdapter.java new file mode 100644 index 0000000000..8da9d22dd9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryIntentAdapter.java @@ -0,0 +1,104 @@ +package info.nightscout.androidaps.plugins.PumpInsight.history; + +import android.content.Intent; + +import java.util.Date; + +import info.nightscout.utils.SP; +import sugar.free.sightparser.handling.HistoryBroadcast; + +import static info.nightscout.androidaps.plugins.PumpInsight.history.PumpIdCache.updatePumpSerialNumber; + +/** + * Created by jamorham on 27/01/2018. + * + * Parse inbound logbook intents + * + */ + +class HistoryIntentAdapter { + + private HistoryLogAdapter logAdapter = new HistoryLogAdapter(); + + private static Date getDateExtra(Intent intent, String name) { + return (Date) intent.getSerializableExtra(name); + } + + private static void log(String msg) { + android.util.Log.e("HistoryIntentAdapter", msg); + } + + static long getRecordUniqueID(long pump_serial_number, long pump_record_id) { + updatePumpSerialNumber(pump_serial_number); + return (pump_serial_number * 10000000) + pump_record_id; + } + + void processTBRIntent(Intent intent) { + + final int pump_tbr_duration = intent.getIntExtra(HistoryBroadcast.EXTRA_DURATION, -1); + final int pump_tbr_percent = intent.getIntExtra(HistoryBroadcast.EXTRA_TBR_AMOUNT, -1); + final int pump_record_id = intent.getIntExtra(HistoryBroadcast.EXTRA_EVENT_NUMBER, -1); + final long pump_serial_number = Long.parseLong(intent.getStringExtra(HistoryBroadcast.EXTRA_PUMP_SERIAL_NUMBER)); + final Date event_time = getDateExtra(intent, HistoryBroadcast.EXTRA_EVENT_TIME); + final Date start_time = getDateExtra(intent, HistoryBroadcast.EXTRA_START_TIME); + + if ((pump_tbr_duration == -1) || (pump_tbr_percent == -1) || (pump_record_id == -1)) { + log("Invalid TBR record!!!"); + return; + } + + final long record_unique_id = getRecordUniqueID(pump_serial_number, pump_record_id); + + // other sanity checks + log("Creating TBR record: " + pump_tbr_percent + "% " + pump_tbr_duration + "m" + " id:" + record_unique_id); + logAdapter.createTBRrecord(start_time, pump_tbr_percent, pump_tbr_duration, record_unique_id); + + } + + void processDeliveredBolusIntent(Intent intent) { + + final String bolus_type = intent.getStringExtra(HistoryBroadcast.EXTRA_BOLUS_TYPE); + final int bolus_id = intent.getIntExtra(HistoryBroadcast.EXTRA_BOLUS_ID,-1); + final int pump_record_id = intent.getIntExtra(HistoryBroadcast.EXTRA_EVENT_NUMBER, -1); + final long pump_serial_number = Long.parseLong(intent.getStringExtra(HistoryBroadcast.EXTRA_PUMP_SERIAL_NUMBER)); + final Date event_time = getDateExtra(intent, HistoryBroadcast.EXTRA_EVENT_TIME); + final Date start_time = getDateExtra(intent, HistoryBroadcast.EXTRA_START_TIME); + final float immediate_amount = intent.getFloatExtra(HistoryBroadcast.EXTRA_IMMEDIATE_AMOUNT, -1); + final float extended_insulin = intent.getFloatExtra(HistoryBroadcast.EXTRA_EXTENDED_AMOUNT, -1); + final int extended_minutes = intent.getIntExtra(HistoryBroadcast.EXTRA_DURATION, -1); + + final long record_unique_id = getRecordUniqueID(pump_serial_number, bolus_id > -1 ? bolus_id : pump_record_id); + + switch (bolus_type) { + case "STANDARD": + if (immediate_amount == -1) { + log("ERROR Standard bolus fails sanity check"); + return; + } + LiveHistory.setStatus(bolus_type + " BOLUS\n" + immediate_amount + "U ", event_time.getTime()); + logAdapter.createStandardBolusRecord(start_time, immediate_amount, record_unique_id); + break; + + case "EXTENDED": + if ((extended_insulin == -1) || (extended_minutes == -1)) { + log("ERROR: Extended bolus fails sanity check"); + return; + } + LiveHistory.setStatus(bolus_type + " BOLUS\n" + extended_insulin + "U over " + extended_minutes + " min, ", event_time.getTime()); + logAdapter.createExtendedBolusRecord(start_time, extended_insulin, extended_minutes, record_unique_id); + break; + + case "MULTIWAVE": + if ((immediate_amount == -1) || (extended_insulin == -1) || (extended_minutes == -1)) { + log("ERROR: Multiwave bolus fails sanity check"); + return; + } + LiveHistory.setStatus(bolus_type + " BOLUS\n" + immediate_amount + "U + " + extended_insulin + "U over " + extended_minutes + " min, ", event_time.getTime()); + logAdapter.createStandardBolusRecord(start_time, immediate_amount, pump_serial_number + pump_record_id); + logAdapter.createExtendedBolusRecord(start_time, extended_insulin, extended_minutes, record_unique_id); + break; + default: + log("ERROR, UNKNWON BOLUS TYPE: " + bolus_type); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryLogAdapter.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryLogAdapter.java new file mode 100644 index 0000000000..73fafd6e6e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryLogAdapter.java @@ -0,0 +1,57 @@ +package info.nightscout.androidaps.plugins.PumpInsight.history; + +import java.util.Date; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.db.ExtendedBolus; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TemporaryBasal; + +/** + * Created by jamorham on 27/01/2018. + * + * Write to the History Log + * + */ + +class HistoryLogAdapter { + + void createTBRrecord(Date eventDate, int percent, int duration, long record_id) { + + final TemporaryBasal temporaryBasal = new TemporaryBasal(); + temporaryBasal.date = eventDate.getTime(); + temporaryBasal.source = Source.PUMP; + temporaryBasal.pumpId = record_id; + temporaryBasal.percentRate = percent; + temporaryBasal.durationInMinutes = duration; + + MainApp.getConfigBuilder().addToHistoryTempBasal(temporaryBasal); + } + + void createExtendedBolusRecord(Date eventDate, double insulin, int durationInMinutes, long record_id) { + + // TODO trap items below minimum period + + final ExtendedBolus extendedBolus = new ExtendedBolus(); + extendedBolus.date = eventDate.getTime(); + extendedBolus.insulin = insulin; + extendedBolus.durationInMinutes = durationInMinutes; + extendedBolus.source = Source.PUMP; + extendedBolus.pumpId = record_id; + + MainApp.getConfigBuilder().addToHistoryExtendedBolus(extendedBolus); + } + + void createStandardBolusRecord(Date eventDate, double insulin, long record_id) { + + //DetailedBolusInfo detailedBolusInfo = DetailedBolusInfoStorage.findDetailedBolusInfo(eventDate.getTime()); + + final DetailedBolusInfo detailedBolusInfo = new DetailedBolusInfo(); + detailedBolusInfo.date = eventDate.getTime(); + detailedBolusInfo.source = Source.PUMP; + detailedBolusInfo.pumpId = record_id; + detailedBolusInfo.insulin = insulin; + MainApp.getConfigBuilder().addToHistoryTreatment(detailedBolusInfo); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryReceiver.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryReceiver.java new file mode 100644 index 0000000000..cad58963b4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/HistoryReceiver.java @@ -0,0 +1,121 @@ +package info.nightscout.androidaps.plugins.PumpInsight.history; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; + +import static info.nightscout.androidaps.plugins.PumpInsight.history.HistoryReceiver.Status.BUSY; +import static info.nightscout.androidaps.plugins.PumpInsight.history.HistoryReceiver.Status.SYNCED; +import static info.nightscout.androidaps.plugins.PumpInsight.history.HistoryReceiver.Status.SYNCING; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_BOLUS_DELIVERED; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_BOLUS_PROGRAMMED; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_END_OF_TBR; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_PUMP_STATUS_CHANGED; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_STILL_SYNCING; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_SYNC_FINISHED; +import static sugar.free.sightparser.handling.HistoryBroadcast.ACTION_SYNC_STARTED; + +/** + * Created by jamorham on 27/01/2018. + */ + +public class HistoryReceiver { + + private static BroadcastReceiver historyReceiver; + private volatile static Status status = Status.IDLE; + private volatile HistoryIntentAdapter intentAdapter; + + public HistoryReceiver() { + initializeHistoryReceiver(); + } + + public static synchronized void registerHistoryReceiver() { + try { + MainApp.instance().unregisterReceiver(historyReceiver); + } catch (Exception e) { + // + } + + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_PUMP_STATUS_CHANGED); + filter.addAction(ACTION_BOLUS_PROGRAMMED); + filter.addAction(ACTION_BOLUS_DELIVERED); + filter.addAction(ACTION_END_OF_TBR); + filter.addAction(ACTION_SYNC_STARTED); + filter.addAction(ACTION_STILL_SYNCING); + filter.addAction(ACTION_SYNC_FINISHED); + + MainApp.instance().registerReceiver(historyReceiver, filter); + } + + // History + + private static void log(String msg) { + android.util.Log.e("INSIGHTPUMPHR", msg); + } + + public static String getStatusString() { + return status.toString(); + } + + private synchronized void initializeHistoryReceiver() { + historyReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, final Intent intent) { + + final String action = intent.getAction(); + if (action == null) return; + + if (intentAdapter == null) { + synchronized (this) { + if (intentAdapter == null) { + intentAdapter = new HistoryIntentAdapter(); + } + } + } + + switch (action) { + + case ACTION_SYNC_STARTED: + status = SYNCING; + break; + case ACTION_STILL_SYNCING: + status = BUSY; + break; + case ACTION_SYNC_FINISHED: + status = SYNCED; + break; + case ACTION_BOLUS_DELIVERED: + intentAdapter.processDeliveredBolusIntent(intent); + break; + case ACTION_END_OF_TBR: + intentAdapter.processTBRIntent(intent); + break; + } + } + }; + } + + enum Status { + IDLE(R.string.insight_history_idle), + SYNCING(R.string.insight_history_syncing), + BUSY(R.string.insight_history_busy), + SYNCED(R.string.insight_history_synced); + + private final int string_id; + + Status(int string_id) { + this.string_id = string_id; + } + + @Override + public String toString() { + return MainApp.instance().getString(string_id); + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/LiveHistory.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/LiveHistory.java new file mode 100644 index 0000000000..ca3848e113 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/LiveHistory.java @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.PumpInsight.history; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers; + +/** + * Created by jamorham on 27/01/2018. + * + * In memory status storage class + */ + +public class LiveHistory { + + private static String status = ""; + private static long status_time = -1; + + public static String getStatus() { + if (status.equals("")) return status; + return status + " " + Helpers.niceTimeScalar(Helpers.msSince(status_time)) + " " + gs(R.string.ago); + } + + public static long getStatusTime() { + return status_time; + } + + static void setStatus(String mystatus, long eventtime) { + if (eventtime > status_time) { + status_time = eventtime; + status = mystatus; + } + } + + private static String gs(int id) { + return MainApp.instance().getString(id); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/PumpIdCache.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/PumpIdCache.java new file mode 100644 index 0000000000..940a7af3cf --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/history/PumpIdCache.java @@ -0,0 +1,33 @@ +package info.nightscout.androidaps.plugins.PumpInsight.history; + +import info.nightscout.utils.SP; + +/** + * Created by jamorham on 01/02/2018. + */ + +public class PumpIdCache { + + private static final String INSIGHT_PUMP_ID_PREF = "insight-pump-id"; + private static long cachedPumpSerialNumber = -1; + + private static void log(String msg) { + android.util.Log.e("PumpIdCache", msg); + } + + static void updatePumpSerialNumber(long pump_serial_number) { + if (pump_serial_number != cachedPumpSerialNumber) { + cachedPumpSerialNumber = pump_serial_number; + log("Updating pump serial number: " + pump_serial_number); + SP.putLong(INSIGHT_PUMP_ID_PREF, cachedPumpSerialNumber); + } + } + + public static long getRecordUniqueID(long record_id) { + if (cachedPumpSerialNumber == -1) { + cachedPumpSerialNumber = SP.getLong(INSIGHT_PUMP_ID_PREF, 0L); + } + return HistoryIntentAdapter.getRecordUniqueID(cachedPumpSerialNumber, record_id); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/Helpers.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/Helpers.java new file mode 100644 index 0000000000..ce86f33e16 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/Helpers.java @@ -0,0 +1,172 @@ +package info.nightscout.androidaps.plugins.PumpInsight.utils; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.PowerManager; +import android.util.Log; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.HashMap; +import java.util.Map; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; + +/** + * Created by jamorham on 24/01/2018. + * + * Useful utility methods from xDrip+ + * + */ + +public class Helpers { + + private static final String TAG = "InsightHelpers"; + + private static final Map rateLimits = new HashMap<>(); + // singletons to avoid repeated allocation + private static DecimalFormatSymbols dfs; + private static DecimalFormat df; + + // return true if below rate limit + public static synchronized boolean ratelimit(String name, int seconds) { + // check if over limit + if ((rateLimits.containsKey(name)) && (tsl() - rateLimits.get(name) < (seconds * 1000))) { + Log.d(TAG, name + " rate limited: " + seconds + " seconds"); + return false; + } + // not over limit + rateLimits.put(name, tsl()); + return true; + } + + public static long tsl() { + return System.currentTimeMillis(); + } + + public static long msSince(long when) { + return (tsl() - when); + } + + public static long msTill(long when) { + return (when - tsl()); + } + + public static boolean checkPackageExists(Context context, String TAG, String packageName) { + try { + final PackageManager pm = context.getPackageManager(); + final PackageInfo pi = pm.getPackageInfo(packageName, 0); + return pi.packageName.equals(packageName); + } catch (PackageManager.NameNotFoundException e) { + return false; + } catch (Exception e) { + Log.wtf(TAG, "Exception trying to determine packages! " + e); + return false; + } + } + + public static boolean runOnUiThreadDelayed(Runnable theRunnable, long delay) { + return new Handler(MainApp.instance().getMainLooper()).postDelayed(theRunnable, delay); + } + + public static PowerManager.WakeLock getWakeLock(final String name, int millis) { + final PowerManager pm = (PowerManager) MainApp.instance().getSystemService(Context.POWER_SERVICE); + if (pm == null) return null; + final PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); + wl.acquire(millis); + return wl; + } + + public static void releaseWakeLock(PowerManager.WakeLock wl) { + if (wl == null) return; + if (wl.isHeld()) wl.release(); + } + + public static String niceTimeSince(long t) { + return niceTimeScalar(msSince(t)); + } + + public static String niceTimeTill(long t) { + return niceTimeScalar(-msSince(t)); + } + + public static String niceTimeScalar(long t) { + String unit = gs(R.string.second); + t = t / 1000; + if (t > 59) { + unit = gs(R.string.minute); + t = t / 60; + if (t > 59) { + unit = gs(R.string.hour); + t = t / 60; + if (t > 24) { + unit = gs(R.string.day); + t = t / 24; + if (t > 28) { + unit = gs(R.string.week); + t = t / 7; + } + } + } + } + if (t != 1) unit = unit + gs(R.string.time_plural); + return qs((double) t, 0) + " " + unit; + } + + private static String gs(int id) { + return MainApp.instance().getString(id); + } + + public static String qs(double x, int digits) { + + if (digits == -1) { + digits = 0; + if (((int) x != x)) { + digits++; + if ((((int) x * 10) / 10 != x)) { + digits++; + if ((((int) x * 100) / 100 != x)) digits++; + } + } + } + + if (dfs == null) { + final DecimalFormatSymbols local_dfs = new DecimalFormatSymbols(); + local_dfs.setDecimalSeparator('.'); + dfs = local_dfs; // avoid race condition + } + + final DecimalFormat this_df; + // use singleton if on ui thread otherwise allocate new as DecimalFormat is not thread safe + if (Thread.currentThread().getId() == 1) { + if (df == null) { + final DecimalFormat local_df = new DecimalFormat("#", dfs); + local_df.setMinimumIntegerDigits(1); + df = local_df; // avoid race condition + } + this_df = df; + } else { + this_df = new DecimalFormat("#", dfs); + } + + this_df.setMaximumFractionDigits(digits); + return this_df.format(x); + } + + public static String niceTimeScalarRedux(long t) { + return niceTimeScalar(t).replaceFirst("^1 ", ""); + } + + public static double roundDouble(double value, int places) { + if (places < 0) throw new IllegalArgumentException("Invalid decimal places"); + BigDecimal bd = new BigDecimal(value); + bd = bd.setScale(places, RoundingMode.HALF_UP); + return bd.doubleValue(); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/StatusItem.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/StatusItem.java new file mode 100644 index 0000000000..395dd51084 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/StatusItem.java @@ -0,0 +1,64 @@ +package info.nightscout.androidaps.plugins.PumpInsight.utils; + +/** + * Created by jamorham on 26/01/2018. + * + * For representing row status items + */ + +public class StatusItem { + + public enum Highlight { + NORMAL, + GOOD, + BAD, + NOTICE, + CRITICAL + } + + public String name; + public String value; + public Highlight highlight; + public String button_name; + public Runnable runnable; + + + public StatusItem(String name, String value) { + this(name, value, Highlight.NORMAL); + } + + public StatusItem() { + this("line-break", "", Highlight.NORMAL); + } + + public StatusItem(String name, Highlight highlight) { + this("heading-break", name, highlight); + } + + public StatusItem(String name, Runnable runnable) { + this("button-break", "", Highlight.NORMAL, name, runnable); + } + + public StatusItem(String name, String value, Highlight highlight) { + this(name, value, highlight, null, null); + } + + public StatusItem(String name, String value, Highlight highlight, String button_name, Runnable runnable) { + this.name = name; + this.value = value; + this.highlight = highlight; + this.button_name = button_name; + this.runnable = runnable; + } + + public StatusItem(String name, Integer value) { + this(name, value, Highlight.NORMAL); + } + + public StatusItem(String name, Integer value, Highlight highlight) { + this.name = name; + this.value = Integer.toString(value); + this.highlight = highlight; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/ui/StatusItemViewAdapter.java b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/ui/StatusItemViewAdapter.java new file mode 100644 index 0000000000..87c40c19de --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PumpInsight/utils/ui/StatusItemViewAdapter.java @@ -0,0 +1,85 @@ +package info.nightscout.androidaps.plugins.PumpInsight.utils.ui; + +import android.app.Activity; +import android.graphics.Color; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.PumpInsight.utils.StatusItem; + +/** + * Created by jamorham on 26/01/2018. + * + * Convert StatusItem to View + */ + +public class StatusItemViewAdapter { + + private final Activity activity; + private final ViewGroup holder; + + public StatusItemViewAdapter(Activity activity, ViewGroup holder) { + this.activity = activity; + this.holder = holder; + } + + public View inflateStatus(StatusItem statusItem) { + if (activity == null) return null; + final View child = activity.getLayoutInflater().inflate(R.layout.insightpump_statuselements, null); + final TextView name = (TextView) child.findViewById(R.id.insightstatuslabel); + final TextView value = (TextView)child.findViewById(R.id.insightstatusvalue); + final TextView spacer = (TextView)child.findViewById(R.id.insightstatusspacer); + final LinearLayout layout = (LinearLayout)child.findViewById(R.id.insightstatuslayout); + + if (statusItem.name.equals("line-break")) { + spacer.setVisibility(View.GONE); + name.setVisibility(View.GONE); + value.setVisibility(View.GONE); + layout.setPadding(10, 10, 10, 10); + } else if (statusItem.name.equals("heading-break")) { + value.setVisibility(View.GONE); + spacer.setVisibility(View.GONE); + name.setText(statusItem.value); + name.setGravity(Gravity.CENTER_HORIZONTAL); + name.setTextColor(Color.parseColor("#fff9c4")); + } else { + name.setText(statusItem.name); + value.setText(statusItem.value); + } + + final int this_color = getHighlightColor(statusItem); + name.setBackgroundColor(this_color); + value.setBackgroundColor(this_color); + spacer.setBackgroundColor(this_color); + + if (this_color != Color.TRANSPARENT) { + name.setTextColor(Color.WHITE); + spacer.setTextColor(Color.WHITE); + } + + if (holder != null) { + holder.addView(child); + } + return child; + } + + private static int getHighlightColor(StatusItem row) { + switch (row.highlight) { + case BAD: + return Color.parseColor("#480000"); + case NOTICE: + return Color.parseColor("#403000"); + case GOOD: + return Color.parseColor("#003000"); + case CRITICAL: + return Color.parseColor("#770000"); + default: + return Color.TRANSPARENT; + } + } + +} diff --git a/app/src/main/res/layout/insightpump_fragment.xml b/app/src/main/res/layout/insightpump_fragment.xml new file mode 100644 index 0000000000..4d3d580336 --- /dev/null +++ b/app/src/main/res/layout/insightpump_fragment.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/insightpump_statuselements.xml b/app/src/main/res/layout/insightpump_statuselements.xml new file mode 100644 index 0000000000..347815e854 --- /dev/null +++ b/app/src/main/res/layout/insightpump_statuselements.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 554b7f759f..a3bf8d7a24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -861,5 +861,45 @@ Recovering from connection loss Not enough insulin for bolus left in reservoir Extended bolus delivery error + Insight + Insight Pump + Status + Changed + PUMP STOPPED + Status Updated + ago + with + Active TBR + min left + Log book + Last Completed Action + min + remaining over + total with + upfront with + Stay always connected + Use Real TBR cancels + Actually cancel a TBR (creates pump alarm) instead of setting 90% or 110% TBR for 15 minutes + IDLE + SYNCING + BUSY + SYNCED + STARTUP + needs + Not connected to companion app! + Companion app does not appear to be installed! + Incompatible companion app, we need version + Unknown + Waiting for code confirmation + Code rejected + App binding + Not authorized + Incompatible + second + minute + hour + day + week + s diff --git a/app/src/main/res/xml/pref_insightpump.xml b/app/src/main/res/xml/pref_insightpump.xml new file mode 100644 index 0000000000..9dc3c24d91 --- /dev/null +++ b/app/src/main/res/xml/pref_insightpump.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/PumpInsight/HelpersTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/PumpInsight/HelpersTest.java new file mode 100644 index 0000000000..885edfac61 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/PumpInsight/HelpersTest.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.PumpInsight; + + +import com.google.common.truth.Truth; + +import org.junit.Test; + +import static info.nightscout.androidaps.plugins.PumpInsight.utils.Helpers.roundDouble; + +/** + * Created by jamorham on 26.01.2018. + */ + +public class HelpersTest { + + @Test + public void checkRounding() throws Exception { + + // TODO more test cases including known precision breakdowns + + Truth.assertThat(roundDouble(Double.parseDouble("0.999999"),0)) + .isEqualTo(1d); + + Truth.assertThat(roundDouble(Double.parseDouble("0.123456"),0)) + .isEqualTo(0d); + + } + + + +} \ No newline at end of file