diff --git a/app/build.gradle b/app/build.gradle index 0867f61e70..fe6ce65442 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -278,6 +278,7 @@ dependencies { implementation "com.joanzapata.iconify:android-iconify-fontawesome:2.2.2" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.madgag.spongycastle:core:1.58.0.0' + implementation(name: "com.atech-software.android.library.wizardpager-1.1.1", ext: "aar") implementation("com.google.android:flexbox:0.3.0") { exclude group: "com.android.support" diff --git a/app/libs/com.atech-software.android.library.wizardpager-1.1.1.aar b/app/libs/com.atech-software.android.library.wizardpager-1.1.1.aar new file mode 100644 index 0000000000..57e2f806bb Binary files /dev/null and b/app/libs/com.atech-software.android.library.wizardpager-1.1.1.aar differ 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 93807543f5..d1c63d284c 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -57,6 +57,7 @@ import info.nightscout.androidaps.plugins.pump.danaR.comm.RecordTypes; import info.nightscout.androidaps.plugins.pump.insight.database.InsightBolusID; import info.nightscout.androidaps.plugins.pump.insight.database.InsightHistoryOffset; import info.nightscout.androidaps.plugins.pump.insight.database.InsightPumpID; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistory; import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin; import info.nightscout.androidaps.utils.JsonHelper; import info.nightscout.androidaps.utils.PercentageSplitter; @@ -86,8 +87,9 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public static final String DATABASE_INSIGHT_HISTORY_OFFSETS = "InsightHistoryOffsets"; public static final String DATABASE_INSIGHT_BOLUS_IDS = "InsightBolusIDs"; public static final String DATABASE_INSIGHT_PUMP_IDS = "InsightPumpIDs"; + public static final String DATABASE_POD_HISTORY = "PodHistory"; - private static final int DATABASE_VERSION = 11; + private static final int DATABASE_VERSION = 12; public static Long earliestDataChange = null; @@ -135,6 +137,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.createTableIfNotExists(connectionSource, InsightHistoryOffset.class); TableUtils.createTableIfNotExists(connectionSource, InsightBolusID.class); TableUtils.createTableIfNotExists(connectionSource, InsightPumpID.class); + TableUtils.createTableIfNotExists(connectionSource, PodHistory.class); database.execSQL("INSERT INTO sqlite_sequence (name, seq) SELECT \"" + DATABASE_INSIGHT_BOLUS_IDS + "\", " + System.currentTimeMillis() + " " + "WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = \"" + DATABASE_INSIGHT_BOLUS_IDS + "\")"); database.execSQL("INSERT INTO sqlite_sequence (name, seq) SELECT \"" + DATABASE_INSIGHT_PUMP_IDS + "\", " + System.currentTimeMillis() + " " + @@ -211,6 +214,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.dropTable(connectionSource, CareportalEvent.class, true); TableUtils.dropTable(connectionSource, ProfileSwitch.class, true); TableUtils.dropTable(connectionSource, TDD.class, true); + TableUtils.dropTable(connectionSource, PodHistory.class, true); TableUtils.createTableIfNotExists(connectionSource, TempTarget.class); TableUtils.createTableIfNotExists(connectionSource, BgReading.class); TableUtils.createTableIfNotExists(connectionSource, DanaRHistoryRecord.class); @@ -220,6 +224,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.createTableIfNotExists(connectionSource, CareportalEvent.class); TableUtils.createTableIfNotExists(connectionSource, ProfileSwitch.class); TableUtils.createTableIfNotExists(connectionSource, TDD.class); + TableUtils.createTableIfNotExists(connectionSource, PodHistory.class); updateEarliestDataChange(0); } catch (SQLException e) { log.error("Unhandled exception", e); @@ -354,6 +359,10 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return getDao(InsightHistoryOffset.class); } + private Dao getDaoPodHistory() throws SQLException { + return getDao(PodHistory.class); + } + public static long roundDateToSec(long date) { long rounded = date - date % 1000; if (rounded != date) @@ -1847,4 +1856,34 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } // ---------------- Food handling --------------- + + // ---------------- PodHistory handling --------------- + + public void createOrUpdate(PodHistory podHistory) { + try { + getDaoPodHistory().createOrUpdate(podHistory); + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + } + + + public List getPodHistoryFromTime(long from, boolean ascending) { + try { + Dao daoPodHistory = getDaoPodHistory(); + List podHistories; + QueryBuilder queryBuilder = daoPodHistory.queryBuilder(); + queryBuilder.orderBy("date", ascending); + //queryBuilder.limit(100L); + Where where = queryBuilder.where(); + where.ge("date", from); + PreparedQuery preparedQuery = queryBuilder.prepare(); + podHistories = daoPodHistory.query(preparedQuery); + return podHistories; + } catch (SQLException e) { + log.error("Unhandled exception", e); + } + return new ArrayList<>(); + } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java index 3089ca1a7c..b13cd74796 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java @@ -67,6 +67,9 @@ public class Notification { public static final int OVER_24H_TIME_CHANGE_REQUESTED = 54; public static final int INVALID_VERSION = 55; public static final int PERMISSION_SYSTEM_WINDOW = 56; + public static final int OMNIPOD_PUMP_ALARM = 57; + public static final int TIME_OR_TIMEZONE_CHANGE = 58; + public static final int OMNIPOD_POD_NOT_ATTACHED = 59; public static final int USERMESSAGE = 1000; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java index efca466951..126b017b5e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/PumpPluginAbstract.java @@ -57,11 +57,14 @@ import io.reactivex.schedulers.Schedulers; public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpInterface, ConstraintsInterface { private CompositeDisposable disposable = new CompositeDisposable(); + protected HasAndroidInjector injector; protected AAPSLogger aapsLogger; protected RxBusWrapper rxBus; protected ActivePluginProvider activePlugin; protected Context context; protected FabricPrivacy fabricPrivacy; + protected ResourceHelper resourceHelper; + protected CommandQueueProvider commandQueue; protected SP sp; /* @@ -99,7 +102,9 @@ public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpI this.activePlugin = activePlugin; this.context = context; this.fabricPrivacy = fabricPrivacy; + this.resourceHelper = resourceHelper; this.sp = sp; + this.commandQueue = commandQueue; pumpDescription.setPumpDescription(pumpType); this.pumpType = pumpType; @@ -109,12 +114,6 @@ public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpI public abstract void initPumpStatusData(); - public abstract void resetRileyLinkConfiguration(); - - public abstract void doTuneUpDevice(); - - public abstract RileyLinkService getRileyLinkService(); - @Override protected void onStart() { super.onStart(); @@ -464,6 +463,7 @@ public abstract class PumpPluginAbstract extends PumpPluginBase implements PumpI public void setPumpType(PumpType pumpType) { this.pumpType = pumpType; + this.pumpDescription.setPumpDescription(pumpType); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java index b704de584f..34a3418902 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/data/PumpStatus.java @@ -49,16 +49,20 @@ public abstract class PumpStatus { public int tempBasalRatio = 0; public int tempBasalRemainMin = 0; public Date tempBasalStart; + private PumpType pumpType; //protected PumpDescription pumpDescription; - public PumpStatus() { + public PumpStatus(PumpType pumpType) { // public PumpStatus(PumpDescription pumpDescription) { // this.pumpDescription = pumpDescription; // this.initSettings(); + this.pumpType = pumpType; } + public abstract void initSettings(); + public void setLastCommunicationToNow() { this.lastDataTime = DateUtil.now(); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java index 4cd32df3f7..3fff66fdf7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/defs/PumpType.java @@ -57,13 +57,13 @@ public enum PumpType { new DoseSettings(0.01d, 15, 24 * 60, 0.05d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 24 * 60, 0d, 250d), PumpCapability.BasalRate_Duration15and30minAllowed, // - 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities, false, false), // + 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities), // AccuChekSolo("Accu-Chek Solo", ManufacturerType.Roche, "Solo", 0.01d, null, // new DoseSettings(0.01d, 15, 24 * 60, 0.05d), // PumpTempBasalType.Percent, new DoseSettings(10, 15, 24 * 60, 0d, 250d), PumpCapability.BasalRate_Duration15and30minAllowed, // - 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities, false, false), // + 0.02d, null, 0.01d, DoseStepSize.InsightBolus, PumpCapability.InsightCapabilities), // // Animas diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java index 50a617a94b..ccb47a7d4a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/dialog/RileyLinkBLEScanActivity.java @@ -41,14 +41,20 @@ import javax.inject.Inject; import info.nightscout.androidaps.R; import info.nightscout.androidaps.activities.NoSplashAppCompatActivity; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.PumpInterface; import info.nightscout.androidaps.logging.AAPSLogger; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.common.ManufacturerType; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.GattAttributes; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.utils.LocationHelper; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.events.EventMedtronicPumpConfigurationChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; import info.nightscout.androidaps.utils.resources.ResourceHelper; import info.nightscout.androidaps.utils.sharedPreferences.SP; @@ -60,8 +66,7 @@ public class RileyLinkBLEScanActivity extends NoSplashAppCompatActivity { @Inject RxBusWrapper rxBus; @Inject ResourceHelper resourceHelper; @Inject RileyLinkUtil rileyLinkUtil; - // TODO change this. Currently verifyConfiguration uses MDT data not only RL - @Inject MedtronicPumpPlugin medtronicPumpPlugin; + @Inject ActivePluginProvider activePlugin; private static final int PERMISSION_REQUEST_COARSE_LOCATION = 30241; // arbitrary. private static final int REQUEST_ENABLE_BT = 30242; // arbitrary @@ -110,9 +115,24 @@ public class RileyLinkBLEScanActivity extends NoSplashAppCompatActivity { sp.putString(RileyLinkConst.Prefs.RileyLinkAddress, bleAddress); - medtronicPumpPlugin.getRileyLinkService().verifyConfiguration(); // force reloading of address + PumpInterface activePump = activePlugin.getActivePump(); - rxBus.send(new EventMedtronicPumpConfigurationChanged()); + if (activePump.manufacturer()== ManufacturerType.Medtronic) { + RileyLinkPumpDevice rileyLinkPump = (RileyLinkPumpDevice)activePump; + rileyLinkPump.getRileyLinkService().verifyConfiguration(); // force reloading of address + + rxBus.send(new EventMedtronicPumpConfigurationChanged()); + + } else if (activePlugin.getActivePump().manufacturer()== ManufacturerType.Insulet) { + if (activePump.model()== PumpType.Insulet_Omnipod_Dash) { + aapsLogger.error("Omnipod Dash not yet implemented."); + } else { + RileyLinkPumpDevice rileyLinkPump = (RileyLinkPumpDevice)activePump; + rileyLinkPump.getRileyLinkService().verifyConfiguration(); // force reloading of address + + rxBus.send(new EventOmnipodPumpValuesChanged()); + } + } finish(); }); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java index 4887d3d4a3..d352250071 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java @@ -21,7 +21,7 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.WakeAndTuneTask; import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; -import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus; +import info.nightscout.androidaps.utils.Round; import info.nightscout.androidaps.utils.sharedPreferences.SP; /** @@ -33,16 +33,14 @@ public abstract class RileyLinkCommunicationManager { @Inject protected AAPSLogger aapsLogger; @Inject protected SP sp; - - @Inject MedtronicPumpStatus medtronicPumpStatus; - @Inject RileyLinkServiceData rileyLinkServiceData; - @Inject ServiceTaskExecutor serviceTaskExecutor; + @Inject protected RileyLinkServiceData rileyLinkServiceData; + @Inject protected ServiceTaskExecutor serviceTaskExecutor; private final int SCAN_TIMEOUT = 1500; private final int ALLOWED_PUMP_UNREACHABLE = 10 * 60 * 1000; // 10 minutes - protected final HasAndroidInjector injector; + public final HasAndroidInjector injector; protected final RFSpy rfspy; protected int receiverDeviceAwakeForMinutes = 1; // override this in constructor of specific implementation protected String receiverDeviceID; // String representation of receiver device (ex. Pump (xxxxxx) or Pod (yyyyyy)) @@ -77,7 +75,7 @@ public abstract class RileyLinkCommunicationManager { return sendAndListen(msg, timeout_ms, repeatCount, 0, extendPreamble_ms, clazz); } - private E sendAndListen(RLMessage msg, int timeout_ms, int repeatCount, int retryCount, Integer extendPreamble_ms, Class clazz) + protected E sendAndListen(RLMessage msg, int timeout_ms, int repeatCount, int retryCount, Integer extendPreamble_ms, Class clazz) throws RileyLinkCommunicationException { // internal flag @@ -129,6 +127,9 @@ public abstract class RileyLinkCommunicationManager { public abstract E createResponseMessage(byte[] payload, Class clazz); + public abstract void setPumpDeviceState(PumpDeviceState pumpDeviceState); + + public void wakeUp(boolean force) { wakeUp(receiverDeviceAwakeForMinutes, force); } @@ -150,7 +151,7 @@ public abstract class RileyLinkCommunicationManager { // **** FIXME: this wakeup doesn't seem to work well... must revisit // receiverDeviceAwakeForMinutes = duration_minutes; - medtronicPumpStatus.setPumpDeviceState(PumpDeviceState.WakingUp); + setPumpDeviceState(PumpDeviceState.WakingUp); if (force) nextWakeUpRequired = 0L; @@ -208,7 +209,7 @@ public abstract class RileyLinkCommunicationManager { double[] scanFrequencies = rileyLinkServiceData.rileyLinkTargetFrequency.getScanFrequencies(); if (scanFrequencies.length == 1) { - return RileyLinkUtil.isSame(scanFrequencies[0], frequency); + return Round.isSame(scanFrequencies[0], frequency); } else { return (scanFrequencies[0] <= frequency && scanFrequencies[scanFrequencies.length - 1] >= frequency); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java index 8fc900aa90..ff524b2ed2 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkUtil.java @@ -18,6 +18,7 @@ import javax.inject.Singleton; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.encoding.Encoding4b6b; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.encoding.Encoding4b6bGeoff; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkEncodingType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkTargetFrequency; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.BleAdvertisedData; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.RLHistoryItem; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceResult; @@ -37,6 +38,9 @@ public class RileyLinkUtil { private RileyLinkEncodingType encoding; private Encoding4b6b encoding4b6b; + // TODO maybe not needed + private RileyLinkTargetFrequency rileyLinkTargetFrequency; + @Inject public RileyLinkUtil() { } @@ -154,4 +158,8 @@ public class RileyLinkUtil { public Encoding4b6b getEncoding4b6b() { return encoding4b6b; } + + public void setRileyLinkTargetFrequency(RileyLinkTargetFrequency rileyLinkTargetFrequency_) { + this.rileyLinkTargetFrequency = rileyLinkTargetFrequency_; + } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java index ef3641fa02..f3f6399570 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RFSpy.java @@ -57,6 +57,7 @@ public class RFSpy { private UUID radioDataUUID = UUID.fromString(GattAttributes.CHARA_RADIO_DATA); private UUID radioVersionUUID = UUID.fromString(GattAttributes.CHARA_RADIO_VERSION); private UUID responseCountUUID = UUID.fromString(GattAttributes.CHARA_RADIO_RESPONSE_COUNT); + private RileyLinkFirmwareVersion firmwareVersion; private String bleVersion; // We don't use it so no need of sofisticated logic private Double currentFrequencyMHz; @@ -68,6 +69,12 @@ public class RFSpy { reader = new RFSpyReader(aapsLogger, rileyLinkBle); } + + public RileyLinkFirmwareVersion getRLVersionCached() { + return firmwareVersion; + } + + public String getBLEVersionCached() { return bleVersion; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java index f8d1909bad..a75e59b17a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/data/RLHistoryItem.java @@ -8,6 +8,8 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLin import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicCommandType; import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; + /** * Created by andy on 5/19/18. @@ -23,6 +25,7 @@ public class RLHistoryItem { private RileyLinkTargetDevice targetDevice; private PumpDeviceState pumpDeviceState; + private OmnipodCommandType omnipodCommandType; public RLHistoryItem(RileyLinkServiceState serviceState, RileyLinkError errorCode, @@ -50,6 +53,13 @@ public class RLHistoryItem { } + public RLHistoryItem(OmnipodCommandType omnipodCommandType) { + this.dateTime = new LocalDateTime(); + this.omnipodCommandType = omnipodCommandType; + source = RLHistoryItemSource.OmnipodCommand; + } + + public LocalDateTime getDateTime() { return dateTime; } @@ -79,6 +89,9 @@ public class RLHistoryItem { case MedtronicCommand: return medtronicCommandType.name(); + case OmnipodCommand: + return omnipodCommandType.name(); + default: return "Unknown Description"; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/defs/RileyLinkPumpDevice.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/defs/RileyLinkPumpDevice.java new file mode 100644 index 0000000000..7ad31c2066 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/defs/RileyLinkPumpDevice.java @@ -0,0 +1,19 @@ +package info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs; + +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkService; + +public interface RileyLinkPumpDevice { + + void setIsBusy(boolean isBusy_); + + boolean isBusy(); + + void resetRileyLinkConfiguration(); + + boolean hasTuneUp(); + + void doTuneUpDevice(); + + RileyLinkService getRileyLinkService(); + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java index 34833f707e..623ecae9ef 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.java @@ -13,7 +13,15 @@ import java.util.Locale; import javax.inject.Inject; import dagger.android.support.DaggerFragment; +import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.PumpDescription; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.plugins.common.ManufacturerType; +import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.pump.common.dialog.RefreshableInterface; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkFirmwareVersion; @@ -23,6 +31,7 @@ import info.nightscout.androidaps.plugins.pump.common.utils.StringUtil; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus; import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; import info.nightscout.androidaps.utils.resources.ResourceHelper; /** @@ -31,11 +40,10 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper; public class RileyLinkStatusGeneralFragment extends DaggerFragment implements RefreshableInterface { - @Inject RileyLinkUtil rileyLinkUtil; - @Inject MedtronicUtil medtronicUtil; - @Inject MedtronicPumpStatus medtronicPumpStatus; + @Inject ActivePluginProvider activePlugin; @Inject ResourceHelper resourceHelper; - @Inject MedtronicPumpPlugin medtronicPumpPlugin; + @Inject MedtronicUtil medtronicUtil; + @Inject AAPSLogger aapsLogger; @Inject RileyLinkServiceData rileyLinkServiceData; TextView connectionStatus; @@ -120,16 +128,16 @@ public class RileyLinkStatusGeneralFragment extends DaggerFragment implements Re } - // TODO add handling for Omnipod pump status + PumpPluginAbstract pumpPlugin = (PumpPluginAbstract)activePlugin.getActivePump(); + + if (pumpPlugin.manufacturer()== ManufacturerType.Medtronic) { + MedtronicPumpStatus medtronicPumpStatus = (MedtronicPumpStatus)pumpPlugin.getPumpStatusData(); - if (medtronicPumpStatus != null) { this.deviceType.setText(resourceHelper.gs(RileyLinkTargetDevice.MedtronicPump.getResourceId())); - this.deviceModel.setText(medtronicPumpPlugin.getPumpDescription().pumpType.getDescription()); + this.deviceModel.setText(pumpPlugin.getPumpType().getDescription()); this.serialNumber.setText(medtronicPumpStatus.serialNumber); this.pumpFrequency.setText(resourceHelper.gs(medtronicPumpStatus.pumpFrequency.equals("medtronic_pump_frequency_us_ca") ? R.string.medtronic_pump_frequency_us_ca : R.string.medtronic_pump_frequency_worldwide)); - // TODO extend when Omnipod used - if (medtronicUtil.getMedtronicPumpModel() != null) this.connectedDevice.setText("Medtronic " + medtronicUtil.getMedtronicPumpModel().getPumpModel()); else @@ -143,7 +151,44 @@ public class RileyLinkStatusGeneralFragment extends DaggerFragment implements Re this.lastDeviceContact.setText(StringUtil.toDateTimeString(new LocalDateTime( medtronicPumpStatus.lastDataTime))); else - this.lastDeviceContact.setText("Never"); + this.lastDeviceContact.setText(resourceHelper.gs(R.string.common_never)); + } else { + + OmnipodPumpStatus omnipodPumpStatus = (OmnipodPumpStatus)pumpPlugin.getPumpStatusData(); + + this.deviceType.setText(resourceHelper.gs(RileyLinkTargetDevice.Omnipod.getResourceId())); + this.deviceModel.setText(pumpPlugin.getPumpType() == PumpType.Insulet_Omnipod ? "Eros" : "Dash"); + + if (pumpPlugin.getPumpType()== PumpType.Insulet_Omnipod_Dash) { + aapsLogger.error("Omnipod Dash not yet supported !!!"); + + this.pumpFrequency.setText("-"); + } else { + + this.pumpFrequency.setText(resourceHelper.gs(R.string.omnipod_frequency)); + + if (omnipodPumpStatus != null) { + + if (omnipodPumpStatus.podAvailable) { + this.serialNumber.setText(omnipodPumpStatus.podLotNumber); + this.connectedDevice.setText(omnipodPumpStatus.pumpType == PumpType.Insulet_Omnipod ? "Eros Pod" : "Dash Pod"); + } else { + this.serialNumber.setText("??"); + this.connectedDevice.setText("-"); + } + + if (rileyLinkServiceData.lastGoodFrequency != null) + this.lastUsedFrequency.setText(String.format(Locale.ENGLISH, "%.2f MHz", + rileyLinkServiceData.lastGoodFrequency)); + + if (omnipodPumpStatus.lastConnection != 0) + this.lastDeviceContact.setText(StringUtil.toDateTimeString(new LocalDateTime( + omnipodPumpStatus.lastDataTime))); + else + this.lastDeviceContact.setText(resourceHelper.gs(R.string.common_never)); + } + } + } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java index 414bc6177b..09273eb719 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/RileyLinkService.java @@ -10,6 +10,8 @@ import org.jetbrains.annotations.NotNull; import javax.inject.Inject; import dagger.android.DaggerService; +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; import info.nightscout.androidaps.logging.AAPSLogger; import info.nightscout.androidaps.logging.LTag; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; @@ -27,6 +29,7 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data. import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus; import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil; +import info.nightscout.androidaps.utils.resources.ResourceHelper; import info.nightscout.androidaps.utils.sharedPreferences.SP; /** @@ -40,9 +43,10 @@ public abstract class RileyLinkService extends DaggerService { @Inject protected Context context; @Inject protected RxBusWrapper rxBus; @Inject protected RileyLinkUtil rileyLinkUtil; - @Inject protected MedtronicUtil medtronicUtil; // TODO should be avoided here as it's MDT + @Inject protected HasAndroidInjector injector; + @Inject protected ResourceHelper resourceHelper; @Inject protected RileyLinkServiceData rileyLinkServiceData; - @Inject protected MedtronicPumpStatus medtronicPumpStatus; + @Inject protected ActivePluginProvider activePlugin; @NotNull protected RileyLinkBLE rileyLinkBLE; // android-bluetooth management, must be set in initRileyLinkServiceData protected BluetoothAdapter bluetoothAdapter; @@ -202,11 +206,13 @@ public abstract class RileyLinkService extends DaggerService { } + + // FIXME: This needs to be run in a session so that is interruptable, has a separate thread, etc. public void doTuneUpDevice() { rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.TuneUpDevice); - medtronicPumpStatus.setPumpDeviceState(PumpDeviceState.Sleeping); + setPumpDeviceState(PumpDeviceState.Sleeping); double lastGoodFrequency = 0.0d; @@ -238,6 +244,9 @@ public abstract class RileyLinkService extends DaggerService { } + public abstract void setPumpDeviceState(PumpDeviceState pumpDeviceState); + + public void disconnectRileyLink() { if (rileyLinkBLE.isConnected()) { @@ -272,4 +281,6 @@ public abstract class RileyLinkService extends DaggerService { else return null; } + + public abstract boolean verifyConfiguration(); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java index d97399e6a1..97a7adc018 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/InitializePumpManagerTask.java @@ -8,14 +8,18 @@ import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.interfaces.ActivePluginProvider; import info.nightscout.androidaps.logging.AAPSLogger; import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.common.ManufacturerType; import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkCommunicationManager; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkTargetFrequency; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkError; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceTransport; +import info.nightscout.androidaps.utils.Round; import info.nightscout.androidaps.utils.sharedPreferences.SP; /** @@ -62,29 +66,53 @@ public class InitializePumpManagerTask extends ServiceTask { lastGoodFrequency = rileyLinkServiceData.lastGoodFrequency; } - RileyLinkCommunicationManager rileyLinkCommunicationManager = ((PumpPluginAbstract) activePlugin.getActivePump()).getRileyLinkService().getDeviceCommunicationManager(); + // TODO Omnipod/Dagger needs refactoring + RileyLinkCommunicationManager rileyLinkCommunicationManager = ((RileyLinkPumpDevice) activePlugin.getActivePump()).getRileyLinkService().getDeviceCommunicationManager(); + + if (activePlugin.getActivePump().manufacturer() == ManufacturerType.Medtronic) { + + if ((lastGoodFrequency > 0.0d) + && rileyLinkCommunicationManager.isValidFrequency(lastGoodFrequency)) { + + rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.RileyLinkReady); + + aapsLogger.info(LTag.PUMPCOMM, "Setting radio frequency to {} MHz", lastGoodFrequency); + + rileyLinkCommunicationManager.setRadioFrequencyForPump(lastGoodFrequency); + + boolean foundThePump = rileyLinkCommunicationManager.tryToConnectToDevice(); + + if (foundThePump) { + rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.PumpConnectorReady); + } else { + rileyLinkServiceData.setServiceState(RileyLinkServiceState.PumpConnectorError, + RileyLinkError.NoContactWithDevice); + rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); + } + + } else { + rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); + } + } else { + + if (!Round.isSame(lastGoodFrequency, RileyLinkTargetFrequency.Omnipod.getScanFrequencies()[0])) { + lastGoodFrequency = RileyLinkTargetFrequency.Omnipod.getScanFrequencies()[0]; + lastGoodFrequency = Math.round(lastGoodFrequency * 1000d) / 1000d; + + rileyLinkServiceData.lastGoodFrequency = lastGoodFrequency; + } - if ((lastGoodFrequency > 0.0d) - && rileyLinkCommunicationManager.isValidFrequency(lastGoodFrequency)) { rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.RileyLinkReady); + rileyLinkUtil.setRileyLinkTargetFrequency(RileyLinkTargetFrequency.Omnipod); aapsLogger.info(LTag.PUMPCOMM, "Setting radio frequency to {} MHz", lastGoodFrequency); rileyLinkCommunicationManager.setRadioFrequencyForPump(lastGoodFrequency); - boolean foundThePump = rileyLinkCommunicationManager.tryToConnectToDevice(); + rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.PumpConnectorReady); - if (foundThePump) { - rileyLinkServiceData.setRileyLinkServiceState(RileyLinkServiceState.PumpConnectorReady); - } else { - rileyLinkServiceData.setServiceState(RileyLinkServiceState.PumpConnectorError, - RileyLinkError.NoContactWithDevice); - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); - } - - } else { - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump, context); } } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java index fb738a241c..370015ab95 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/ResetRileyLinkConfigurationTask.java @@ -4,10 +4,11 @@ import javax.inject.Inject; import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.interfaces.ActivePluginProvider; -import info.nightscout.androidaps.interfaces.PumpInterface; import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.common.ManufacturerType; import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceTransport; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.events.EventRefreshButtonState; @@ -21,9 +22,6 @@ public class ResetRileyLinkConfigurationTask extends PumpTask { @Inject ActivePluginProvider activePlugin; @Inject RxBusWrapper rxBus; - private static final String TAG = "ResetRileyLinkTask"; - - public ResetRileyLinkConfigurationTask(HasAndroidInjector injector) { super(injector); } @@ -36,12 +34,16 @@ public class ResetRileyLinkConfigurationTask extends PumpTask { @Override public void run() { - PumpPluginAbstract pump = (PumpPluginAbstract) activePlugin.getActivePump(); + RileyLinkPumpDevice pumpAbstract = (RileyLinkPumpDevice)activePlugin.getActivePump(); + rxBus.send(new EventRefreshButtonState(false)); - MedtronicPumpPlugin.isBusy = true; - pump.resetRileyLinkConfiguration(); - MedtronicPumpPlugin.isBusy = false; + + pumpAbstract.setIsBusy(true); + pumpAbstract.resetRileyLinkConfiguration(); + pumpAbstract.setIsBusy(false); + rxBus.send(new EventRefreshButtonState(true)); } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java index d7eed28ef4..219da8bf3f 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/service/tasks/WakeAndTuneTask.java @@ -6,6 +6,7 @@ import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.interfaces.ActivePluginProvider; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.data.ServiceTransport; import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.events.EventRefreshButtonState; @@ -33,11 +34,11 @@ public class WakeAndTuneTask extends PumpTask { @Override public void run() { - PumpPluginAbstract pump = (PumpPluginAbstract) activePlugin.getActivePump(); + RileyLinkPumpDevice pumpDevice = (RileyLinkPumpDevice)activePlugin.getActivePump(); rxBus.send(new EventRefreshButtonState(false)); - MedtronicPumpPlugin.isBusy = true; - pump.doTuneUpDevice(); - MedtronicPumpPlugin.isBusy = false; + pumpDevice.setIsBusy(true); + pumpDevice.doTuneUpDevice(); + pumpDevice.setIsBusy(false); rxBus.send(new EventRefreshButtonState(true)); } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java index 4bb5609f33..9d4b89675e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicPumpPlugin.java @@ -57,6 +57,7 @@ import info.nightscout.androidaps.plugins.pump.common.defs.PumpDriverState; import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ResetRileyLinkConfigurationTask; @@ -95,7 +96,7 @@ import info.nightscout.androidaps.utils.sharedPreferences.SP; * @author Andy Rozman (andy.rozman@gmail.com) */ @Singleton -public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInterface { +public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInterface, RileyLinkPumpDevice { private final SP sp; private final RileyLinkUtil rileyLinkUtil; @@ -234,6 +235,11 @@ public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInter rileyLinkMedtronicService.resetRileyLinkConfiguration(); } + @Override + public boolean hasTuneUp() { + return true; + } + @Override public void doTuneUpDevice() { rileyLinkMedtronicService.doTuneUpDevice(); } @@ -329,6 +335,11 @@ public class MedtronicPumpPlugin extends PumpPluginAbstract implements PumpInter } + @Override + public void setIsBusy(boolean isBusy_) { + isBusy = isBusy_; + } + @Override public boolean isBusy() { if (displayConnectionMessages) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java index 348110c671..086dc92cd7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicCommunicationManager.java @@ -92,6 +92,11 @@ public class MedtronicCommunicationManager extends RileyLinkCommunicationManager return (E) pumpMessage; } + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.medtronicPumpStatus.setPumpDeviceState(pumpDeviceState); + } + public void setDoWakeUpBeforeCommand(boolean doWakeUp) { this.doWakeUpBeforeCommand = doWakeUp; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java index 4d81eccc3a..496f2af06b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.java @@ -56,6 +56,7 @@ import info.nightscout.androidaps.plugins.treatments.Treatment; import info.nightscout.androidaps.plugins.treatments.TreatmentService; import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.Round; import info.nightscout.androidaps.utils.sharedPreferences.SP; @@ -659,7 +660,7 @@ public class MedtronicHistoryData { Treatment treatment = (Treatment) dbObjectBase; - if (RileyLinkUtil.isSame(treatment.insulin, 0d)) { + if (Round.isSame(treatment.insulin, 0d)) { removeList.add(dbObjectBase); } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java index 02f08e93d7..f7686fa824 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/driver/MedtronicPumpStatus.java @@ -60,13 +60,12 @@ public class MedtronicPumpStatus extends PumpStatus { @Inject - public MedtronicPumpStatus( - ResourceHelper resourceHelper, + public MedtronicPumpStatus(ResourceHelper resourceHelper, SP sp, RxBusWrapper rxBus, RileyLinkUtil rileyLinkUtil ) { - super(); + super(PumpType.Medtronic_522_722); this.resourceHelper = resourceHelper; this.sp = sp; this.rxBus = rxBus; @@ -75,7 +74,7 @@ public class MedtronicPumpStatus extends PumpStatus { } - private void initSettings() { + public void initSettings() { this.activeProfileName = "STD"; this.reservoirRemainingUnits = 75d; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java index ac88f946aa..7c6a55d092 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java @@ -38,8 +38,6 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper; */ public class RileyLinkMedtronicService extends RileyLinkService { - @Inject HasAndroidInjector injector; - @Inject ResourceHelper resourceHelper; @Inject MedtronicPumpPlugin medtronicPumpPlugin; @Inject MedtronicUtil medtronicUtil; @Inject MedtronicUIPostprocessor medtronicUIPostprocessor; @@ -120,6 +118,12 @@ public class RileyLinkMedtronicService extends RileyLinkService { } + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.medtronicPumpStatus.setPumpDeviceState(pumpDeviceState); + } + + public MedtronicUIComm getMedtronicUIComm() { return medtronicUIComm; } @@ -236,7 +240,7 @@ public class RileyLinkMedtronicService extends RileyLinkService { } else { PumpType pumpType = medtronicPumpStatus.getMedtronicPumpMap().get(pumpTypePart); medtronicPumpStatus.medtronicDeviceType = medtronicPumpStatus.getMedtronicDeviceTypeMap().get(pumpTypePart); - medtronicPumpPlugin.getPumpDescription().setPumpDescription(pumpType); + medtronicPumpPlugin.setPumpType(pumpType); if (pumpTypePart.startsWith("7")) medtronicPumpStatus.reservoirFullUnits = 300; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodFragment.kt new file mode 100644 index 0000000000..fce29225d4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodFragment.kt @@ -0,0 +1,458 @@ +package info.nightscout.androidaps.plugins.pump.omnipod + +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.os.Handler +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dagger.android.support.DaggerFragment +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.events.EventPreferenceChange +import info.nightscout.androidaps.interfaces.ActivePluginProvider +import info.nightscout.androidaps.interfaces.CommandQueueProvider +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.logging.LTag +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkTargetDevice +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.dialog.RileyLinkStatusActivity +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodStatusRequest +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.PodManagementActivity +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodAcknowledgeAlertsChanged +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodDeviceStatusChange +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodRefreshButtonState +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil +import info.nightscout.androidaps.queue.Callback +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.Round +import info.nightscout.androidaps.utils.T +import info.nightscout.androidaps.utils.WarnColors +import info.nightscout.androidaps.utils.alertDialogs.OKDialog +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.sharedPreferences.SP +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.omnipod_fragment.* +import javax.inject.Inject + +class OmnipodFragment : DaggerFragment() { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var mainApp: MainApp + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var commandQueue: CommandQueueProvider + @Inject lateinit var activePlugin: ActivePluginProvider + @Inject lateinit var omnipodPumpPlugin: OmnipodPumpPlugin + @Inject lateinit var warnColors: WarnColors + @Inject lateinit var omnipodPumpStatus: OmnipodPumpStatus + @Inject lateinit var sp: SP + @Inject lateinit var omnipodUtil: OmnipodUtil + + private var disposable: CompositeDisposable = CompositeDisposable() + + private val loopHandler = Handler() + private lateinit var refreshLoop: Runnable + + operator fun CompositeDisposable.plusAssign(disposable: Disposable) { + add(disposable) + } + + init { + refreshLoop = Runnable { + activity?.runOnUiThread { updateGUI() } + loopHandler.postDelayed(refreshLoop, T.mins(1).msecs()) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.omnipod_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + omnipod_rl_status.text = resourceHelper.gs(RileyLinkServiceState.NotStarted.getResourceId(RileyLinkTargetDevice.Omnipod)) + + omnipod_pod_status.setTextColor(Color.WHITE) + omnipod_pod_status.text = "{fa-bed}" + + omnipod_pod_mgmt.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() == true) { + startActivity(Intent(context, PodManagementActivity::class.java)) + } else { + displayNotConfiguredDialog() + } + } + + omnipod_refresh.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { + OmnipodUtil.displayNotConfiguredDialog(context) + } else { + omnipod_refresh.isEnabled = false + omnipodPumpPlugin.addPodStatusRequest(OmnipodStatusRequest.GetPodState); + commandQueue.readStatus("Clicked Refresh", object : Callback() { + override fun run() { + activity?.runOnUiThread { omnipod_refresh.isEnabled = true } + } + }) + } + } + + omnipod_stats.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() == true) { + startActivity(Intent(context, RileyLinkStatusActivity::class.java)) + } else { + displayNotConfiguredDialog() + } + } + + omnipod_pod_active_alerts_ack.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { + displayNotConfiguredDialog() + } else { + omnipod_pod_active_alerts_ack.isEnabled = false + omnipodPumpPlugin.addPodStatusRequest(OmnipodStatusRequest.AcknowledgeAlerts); + commandQueue.readStatus("Clicked Alert Ack", null) + } + } + + omnipod_pod_debug.setOnClickListener { + if (omnipodPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { + displayNotConfiguredDialog() + } else { + omnipod_pod_debug.isEnabled = false + omnipodPumpPlugin.addPodStatusRequest(OmnipodStatusRequest.GetPodPulseLog); + commandQueue.readStatus("Clicked Refresh", object : Callback() { + override fun run() { + activity?.runOnUiThread { omnipod_pod_debug.isEnabled = true } + } + }) + } + } + + omnipod_lastconnection.setTextColor(Color.WHITE) + + setVisibilityOfPodDebugButton() + + updateGUI() + } + + override fun onResume() { + super.onResume() + loopHandler.postDelayed(refreshLoop, T.mins(1).msecs()) + disposable += rxBus + .toObservable(EventOmnipodRefreshButtonState::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ omnipod_refresh.isEnabled = it.newState }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventOmnipodDeviceStatusChange::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + aapsLogger.info(LTag.PUMP, "onStatusEvent(EventOmnipodDeviceStatusChange): {}", it) + setDeviceStatus() + }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventOmnipodPumpValuesChanged::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ updateGUI() }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventOmnipodAcknowledgeAlertsChanged::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ updateAcknowledgeAlerts() }, { fabricPrivacy.logException(it) }) + disposable += rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(Schedulers.io()) + .subscribe({ event -> + setVisibilityOfPodDebugButton() + }, { fabricPrivacy.logException(it) }) + } + + fun setVisibilityOfPodDebugButton() { + val isEnabled = sp.getBoolean(OmnipodConst.Prefs.PodDebuggingOptionsEnabled, false) + + if (isEnabled) + omnipod_pod_debug.visibility = View.VISIBLE + else + omnipod_pod_debug.visibility = View.GONE + } + + private fun displayNotConfiguredDialog() { + context?.let { + OKDialog.show(it, resourceHelper.gs(R.string.combo_warning), + resourceHelper.gs(R.string.omnipod_error_operation_not_possible_no_configuration), null) + } + } + + override fun onPause() { + super.onPause() + disposable.clear() + loopHandler.removeCallbacks(refreshLoop) + } + + @Synchronized + private fun setDeviceStatus() { + //val omnipodPumpStatus: OmnipodPumpStatus = OmnipodUtil.getPumpStatus() + // omnipodPumpStatus.rileyLinkServiceState = checkStatusSet(omnipodPumpStatus.rileyLinkServiceState, + // RileyLinkUtil.getServiceState()) as RileyLinkServiceState? + + aapsLogger.info(LTag.PUMP, "setDeviceStatus: [pumpStatus={}]", omnipodPumpStatus) + + val resourceId = omnipodPumpStatus.rileyLinkServiceState.getResourceId(RileyLinkTargetDevice.Omnipod) + val rileyLinkError = omnipodPumpPlugin.rileyLinkService?.error + + omnipod_rl_status.text = + when { + omnipodPumpStatus.rileyLinkServiceState == RileyLinkServiceState.NotStarted -> resourceHelper.gs(resourceId) + omnipodPumpStatus.rileyLinkServiceState.isConnecting -> "{fa-bluetooth-b spin} " + resourceHelper.gs(resourceId) + omnipodPumpStatus.rileyLinkServiceState.isError && rileyLinkError == null -> "{fa-bluetooth-b} " + resourceHelper.gs(resourceId) + omnipodPumpStatus.rileyLinkServiceState.isError && rileyLinkError != null -> "{fa-bluetooth-b} " + resourceHelper.gs(rileyLinkError.getResourceId(RileyLinkTargetDevice.MedtronicPump)) + else -> "{fa-bluetooth-b} " + resourceHelper.gs(resourceId) + } + omnipod_rl_status.setTextColor(if (rileyLinkError != null) Color.RED else Color.WHITE) + + // omnipodPumpStatus.rileyLinkError = checkStatusSet(omnipodPumpStatus.rileyLinkError, + // RileyLinkUtil.getError()) as RileyLinkError? + + omnipod_errors.text = + omnipodPumpStatus.rileyLinkError?.let { + resourceHelper.gs(it.getResourceId(RileyLinkTargetDevice.Omnipod)) + } ?: "-" + + val driverState = omnipodUtil.getDriverState(); + + aapsLogger.info(LTag.PUMP, "getDriverState: [driverState={}]", driverState) + + if (driverState == OmnipodDriverState.NotInitalized) { + omnipod_pod_address.text = resourceHelper.gs(R.string.omnipod_pod_name_no_info) + omnipod_pod_expiry.text = "-" + omnipod_pod_status.text = resourceHelper.gs(R.string.omnipod_pod_not_initalized) + omnipodPumpStatus.podAvailable = false + omnipodPumpStatus.podNumber == null + } else if (driverState == OmnipodDriverState.Initalized_NoPod) { + omnipod_pod_address.text = resourceHelper.gs(R.string.omnipod_pod_name_no_info) + omnipod_pod_expiry.text = "-" + omnipod_pod_status.text = resourceHelper.gs(R.string.omnipod_pod_no_pod_connected) + omnipodPumpStatus.podAvailable = false + omnipodPumpStatus.podNumber == null + } else if (driverState == OmnipodDriverState.Initalized_PodInitializing) { + omnipod_pod_address.text = omnipodPumpStatus.podSessionState.address.toString() + omnipod_pod_expiry.text = "-" + omnipod_pod_status.text = omnipodPumpStatus.podSessionState.getSetupProgress().name + omnipodPumpStatus.podAvailable = false + omnipodPumpStatus.podNumber == omnipodPumpStatus.podSessionState.address.toString() + } else { + omnipodPumpStatus.podLotNumber = "" + omnipodPumpStatus.podSessionState.lot + omnipodPumpStatus.podAvailable = true + omnipod_pod_address.text = omnipodPumpStatus.podSessionState.address.toString() + omnipod_pod_expiry.text = omnipodPumpStatus.podSessionState.expiryDateAsString + omnipodPumpStatus.podNumber = omnipodPumpStatus.podSessionState.address.toString() + + //pumpStatus.podSessionState = checkStatusSet(pumpStatus.podSessionState, + // OmnipodUtil.getPodSessionState()) as PodSessionState? + + var podDeviceState = omnipodPumpStatus.podDeviceState + + when (podDeviceState) { + null, + PodDeviceState.Sleeping -> omnipod_pod_status.text = "{fa-bed} " // + pumpStatus.pumpDeviceState.name()); + PodDeviceState.NeverContacted, + PodDeviceState.WakingUp, + PodDeviceState.PumpUnreachable, + PodDeviceState.ErrorWhenCommunicating, + PodDeviceState.TimeoutWhenCommunicating, + PodDeviceState.InvalidConfiguration -> omnipod_pod_status.text = " " + resourceHelper.gs(podDeviceState.resourceId) + + PodDeviceState.Active -> { + + omnipod_pod_status.text = "Active"; +// val cmd = OmnipodUtil.getCurrentCommand() +// if (cmd == null) +// omnipod_pod_status.text = " " + resourceHelper.gs(pumpStatus.pumpDeviceState.resourceId) +// else { +// aapsLogger.debug(LTag.PUMP,"Command: " + cmd) +// val cmdResourceId = cmd.resourceId +// if (cmd == MedtronicCommandType.GetHistoryData) { +// omnipod_pod_status.text = OmnipodUtil.frameNumber?.let { +// resourceHelper.gs(cmdResourceId, OmnipodUtil.pageNumber, OmnipodUtil.frameNumber) +// } +// ?: resourceHelper.gs(R.string.medtronic_cmd_desc_get_history_request, OmnipodUtil.pageNumber) +// } else { +// omnipod_pod_status.text = " " + (cmdResourceId?.let { resourceHelper.gs(it) } +// ?: cmd.getCommandDescription()) +// } +// } + } + + else -> aapsLogger.warn(LTag.PUMP, "Unknown pump state: " + omnipodPumpStatus.podDeviceState) + } + + } + +// pumpStatus.pumpDeviceState = checkStatusSet(pumpStatus.pumpDeviceState, +// OmnipodUtil.getPumpDeviceState()) as PumpDeviceState? +// +// when (pumpStatus.pumpDeviceState) { +// null, +// PumpDeviceState.Sleeping -> omnipod_pod_status.text = "{fa-bed} " // + pumpStatus.pumpDeviceState.name()); +// PumpDeviceState.NeverContacted, +// PumpDeviceState.WakingUp, +// PumpDeviceState.PumpUnreachable, +// PumpDeviceState.ErrorWhenCommunicating, +// PumpDeviceState.TimeoutWhenCommunicating, +// PumpDeviceState.InvalidConfiguration -> omnipod_pod_status.text = " " + resourceHelper.gs(pumpStatus.pumpDeviceState.resourceId) +// PumpDeviceState.Active -> { +// val cmd = OmnipodUtil.getCurrentCommand() +// if (cmd == null) +// omnipod_pod_status.text = " " + resourceHelper.gs(pumpStatus.pumpDeviceState.resourceId) +// else { +// aapsLogger.debug(LTag.PUMP,"Command: " + cmd) +// val cmdResourceId = cmd.resourceId +// if (cmd == MedtronicCommandType.GetHistoryData) { +// omnipod_pod_status.text = OmnipodUtil.frameNumber?.let { +// resourceHelper.gs(cmdResourceId, OmnipodUtil.pageNumber, OmnipodUtil.frameNumber) +// } +// ?: resourceHelper.gs(R.string.medtronic_cmd_desc_get_history_request, OmnipodUtil.pageNumber) +// } else { +// omnipod_pod_status.text = " " + (cmdResourceId?.let { resourceHelper.gs(it) } +// ?: cmd.getCommandDescription()) +// } +// } +// } +// else -> aapsLogger.warn(LTag.PUMP,"Unknown pump state: " + pumpStatus.pumpDeviceState) +// } + + val status = commandQueue.spannedStatus() + if (status.toString() == "") { + omnipod_queue.visibility = View.GONE + } else { + omnipod_queue.visibility = View.VISIBLE + omnipod_queue.text = status + } + } + + private fun checkStatusSet(object1: Any?, object2: Any?): Any? { + return if (object1 == null) { + object2 + } else { + if (object1 != object2) { + object2 + } else + object1 + } + } + + // GUI functions + fun updateGUI() { + val plugin = omnipodPumpPlugin + //val omnipodPumpStatus = OmnipodUtil.getPumpStatus() + var pumpType = omnipodPumpStatus.pumpType + + if (pumpType == null) { + aapsLogger.warn(LTag.PUMP, "PumpType was not set, reseting to Omnipod.") + pumpType = PumpType.Insulet_Omnipod; + } + + setDeviceStatus() + + if (omnipodPumpStatus.podAvailable) { + // last connection + if (omnipodPumpStatus.lastConnection != 0L) { + //val minAgo = DateUtil.minAgo(pumpStatus.lastConnection) + val min = (System.currentTimeMillis() - omnipodPumpStatus.lastConnection) / 1000 / 60 + if (omnipodPumpStatus.lastConnection + 60 * 1000 > System.currentTimeMillis()) { + omnipod_lastconnection.setText(R.string.combo_pump_connected_now) + //omnipod_lastconnection.setTextColor(Color.WHITE) + } else { //if (pumpStatus.lastConnection + 30 * 60 * 1000 < System.currentTimeMillis()) { + + if (min < 60) { + omnipod_lastconnection.text = resourceHelper.gs(R.string.minago, min) + } else if (min < 1440) { + val h = (min / 60).toInt() + omnipod_lastconnection.text = (resourceHelper.gq(R.plurals.objective_hours, h, h) + " " + + resourceHelper.gs(R.string.ago)) + } else { + val h = (min / 60).toInt() + val d = h / 24 + // h = h - (d * 24); + omnipod_lastconnection.text = (resourceHelper.gq(R.plurals.objective_days, d, d) + " " + + resourceHelper.gs(R.string.ago)) + } + //omnipod_lastconnection.setTextColor(Color.RED) + } +// } else { +// omnipod_lastconnection.text = minAgo +// //omnipod_lastconnection.setTextColor(Color.WHITE) +// } + } + + // last bolus + val bolus = omnipodPumpStatus.lastBolusAmount + val bolusTime = omnipodPumpStatus.lastBolusTime + if (bolus != null && bolusTime != null && omnipodPumpStatus.podAvailable) { + val agoMsc = System.currentTimeMillis() - omnipodPumpStatus.lastBolusTime.time + val bolusMinAgo = agoMsc.toDouble() / 60.0 / 1000.0 + val unit = resourceHelper.gs(R.string.insulin_unit_shortname) + val ago: String + if (agoMsc < 60 * 1000) { + ago = resourceHelper.gs(R.string.combo_pump_connected_now) + } else if (bolusMinAgo < 60) { + ago = DateUtil.minAgo(resourceHelper, omnipodPumpStatus.lastBolusTime.time) + } else { + ago = DateUtil.hourAgo(omnipodPumpStatus.lastBolusTime.time, resourceHelper) + } + omnipod_lastbolus.text = resourceHelper.gs(R.string.omnipod_last_bolus, pumpType.determineCorrectBolusSize(bolus), unit, ago) + } else { + omnipod_lastbolus.text = "" + } + + // base basal rate + omnipod_basabasalrate.text = resourceHelper.gs(R.string.pump_basebasalrate, pumpType.determineCorrectBasalSize(plugin.baseBasalRate)) + + omnipod_tempbasal.text = activePlugin.activeTreatments + .getTempBasalFromHistory(System.currentTimeMillis())?.toStringFull() ?: "" + + // reservoir + if (Round.isSame(omnipodPumpStatus.reservoirRemainingUnits, 75.0)) { + omnipod_reservoir.text = resourceHelper.gs(R.string.omnipod_reservoir_over50) + } else { + omnipod_reservoir.text = resourceHelper.gs(R.string.omnipod_reservoir_left, omnipodPumpStatus.reservoirRemainingUnits) + } + warnColors.setColorInverse(omnipod_reservoir, omnipodPumpStatus.reservoirRemainingUnits, 50.0, 20.0) + + } else { + omnipod_basabasalrate.text = "" + omnipod_reservoir.text = "" + omnipod_tempbasal.text = "" + omnipod_lastbolus.text = "" + omnipod_lastconnection.text = "" + omnipod_lastconnection.setTextColor(Color.WHITE) + } + + omnipod_errors.text = omnipodPumpStatus.errorInfo + + updateAcknowledgeAlerts() + + omnipod_refresh.isEnabled = omnipodPumpStatus.podAvailable + + } + + private fun updateAcknowledgeAlerts() { + omnipod_pod_active_alerts_ack.isEnabled = omnipodPumpStatus.ackAlertsAvailable + omnipod_pod_active_alerts.text = omnipodPumpStatus.ackAlertsText + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodPumpPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodPumpPlugin.java new file mode 100644 index 0000000000..62adc49855 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodPumpPlugin.java @@ -0,0 +1,1034 @@ +package info.nightscout.androidaps.plugins.pump.omnipod; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.SystemClock; + +import androidx.annotation.NonNull; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.activities.ErrorHelperActivity; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TemporaryBasal; +import info.nightscout.androidaps.events.EventPreferenceChange; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.CommandQueueProvider; +import info.nightscout.androidaps.interfaces.PluginDescription; +import info.nightscout.androidaps.interfaces.PluginType; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.general.actions.defs.CustomAction; +import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType; +import info.nightscout.androidaps.plugins.general.overview.events.EventDismissNotification; +import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification; +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification; +import info.nightscout.androidaps.plugins.pump.common.PumpPluginAbstract; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpDriverState; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkPumpDevice; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ResetRileyLinkConfigurationTask; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ServiceTaskExecutor; +import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCustomActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodPumpPluginInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodStatusRequest; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.ui.OmnipodUIComm; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.ui.OmnipodUITask; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodRefreshButtonState; +import info.nightscout.androidaps.plugins.pump.omnipod.service.RileyLinkOmnipodService; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.FabricPrivacy; +import info.nightscout.androidaps.utils.Round; +import info.nightscout.androidaps.utils.TimeChangeType; +import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.sharedPreferences.SP; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Created by andy on 23.04.18. + * + * @author Andy Rozman (andy.rozman@gmail.com) + */ +@Singleton +public class OmnipodPumpPlugin extends PumpPluginAbstract implements OmnipodPumpPluginInterface, RileyLinkPumpDevice { + + // TODO Dagger (maybe done) + private static OmnipodPumpPlugin plugin = null; + private RileyLinkServiceData rileyLinkServiceData; + private ServiceTaskExecutor serviceTaskExecutor; + private RileyLinkOmnipodService rileyLinkOmnipodService; + private OmnipodUtil omnipodUtil; + protected OmnipodPumpStatus omnipodPumpStatus = null; + protected OmnipodUIComm omnipodUIComm; + + private CompositeDisposable disposable = new CompositeDisposable(); + + + // variables for handling statuses and history + protected boolean firstRun = true; + protected boolean isRefresh = false; + private boolean isBasalProfileInvalid = false; + private boolean basalProfileChanged = false; + private boolean isInitialized = false; + protected OmnipodCommunicationManagerInterface omnipodCommunicationManager; + + public static boolean isBusy = false; + protected List busyTimestamps = new ArrayList<>(); + protected boolean sentIdToFirebase = false; + protected boolean hasTimeDateOrTimeZoneChanged = false; + private int timeChangeRetries = 0; + + private Profile currentProfile; + + boolean omnipodServiceRunning = false; + + private long nextPodCheck = 0L; + //OmnipodDriverState driverState = OmnipodDriverState.NotInitalized; + + @Inject + public OmnipodPumpPlugin( + HasAndroidInjector injector, + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + Context context, + ResourceHelper resourceHelper, + ActivePluginProvider activePlugin, + SP sp, + OmnipodUtil omnipodUtil, + OmnipodPumpStatus omnipodPumpStatus, + CommandQueueProvider commandQueue, + FabricPrivacy fabricPrivacy, + RileyLinkServiceData rileyLinkServiceData, + ServiceTaskExecutor serviceTaskExecutor) { + + super(new PluginDescription() // + .mainType(PluginType.PUMP) // + .fragmentClass(OmnipodFragment.class.getName()) // + .pluginName(R.string.omnipod_name) // + .shortName(R.string.omnipod_name_short) // + .preferencesId(R.xml.pref_omnipod) // + .description(R.string.description_pump_omnipod), // + PumpType.Insulet_Omnipod, + injector, resourceHelper, aapsLogger, commandQueue, rxBus, activePlugin, sp, context, fabricPrivacy + ); + this.rileyLinkServiceData = rileyLinkServiceData; + this.serviceTaskExecutor = serviceTaskExecutor; + + displayConnectionMessages = false; + OmnipodPumpPlugin.plugin = this; + this.omnipodUtil = omnipodUtil; + this.omnipodPumpStatus = omnipodPumpStatus; + + //OmnipodUtil.setDriverState(); + +// TODO loop +// if (OmnipodUtil.isOmnipodEros()) { +// OmnipodUtil.setPlugin(this); +// OmnipodUtil.setOmnipodPodType(OmnipodPodType.Eros); +// OmnipodUtil.setPumpType(PumpType.Insulet_Omnipod); +// } + +// // TODO ccc + + + serviceConnection = new ServiceConnection() { + + @Override + public void onServiceDisconnected(ComponentName name) { + + aapsLogger.debug(LTag.PUMP, "RileyLinkOmnipodService is disconnected"); + rileyLinkOmnipodService = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + + aapsLogger.debug(LTag.PUMP, "RileyLinkOmnipodService is connected"); + RileyLinkOmnipodService.LocalBinder mLocalBinder = (RileyLinkOmnipodService.LocalBinder) service; + rileyLinkOmnipodService = mLocalBinder.getServiceInstance(); + + new Thread(() -> { + + for (int i = 0; i < 20; i++) { + SystemClock.sleep(5000); + + aapsLogger.debug(LTag.PUMP, "Starting Omnipod-RileyLink service"); + if (rileyLinkOmnipodService.setNotInPreInit()) { + break; + } + } + + +// if (OmnipodPumpPlugin.this.omnipodPumpStatus != null) { +// +// aapsLogger.debug(LTag.PUMP, "Starting OmniPod-RileyLink service"); +// if (omnipodService.setNotInPreInit()) { +// if (omnipodCommunicationManager == null) { +// omnipodCommunicationManager = AapsOmnipodManager.getInstance(); +// omnipodCommunicationManager.setPumpStatus(OmnipodPumpPlugin.this.omnipodPumpStatus); +// omnipodServiceRunning = true; +// } else { +// omnipodCommunicationManager.setPumpStatus(OmnipodPumpPlugin.this.omnipodPumpStatus); +// } +// +// omnipodUtil.setOmnipodPodType(OmnipodPodType.Eros); +// //omnipodUtil.setPlugin(OmnipodPumpPlugin.this); +// +// omnipodUIComm = new OmnipodUIComm(omnipodCommunicationManager, plugin, OmnipodPumpPlugin.this.omnipodPumpStatus); +// break; +// } +// } +// +// SystemClock.sleep(5000); + //} + }).start(); + } + }; + } + + protected OmnipodPumpPlugin(PluginDescription pluginDescription, PumpType pumpType, + HasAndroidInjector injector, + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + Context context, + ResourceHelper resourceHelper, + ActivePluginProvider activePlugin, + info.nightscout.androidaps.utils.sharedPreferences.SP sp, + CommandQueueProvider commandQueue, + FabricPrivacy fabricPrivacy) { + super(pluginDescription, pumpType, injector, resourceHelper, aapsLogger, commandQueue, rxBus, activePlugin, sp, context, fabricPrivacy); + +// this.rileyLinkUtil = rileyLinkUtil; +// this.medtronicUtil = medtronicUtil; +// this.sp = sp; +// this.medtronicPumpStatus = medtronicPumpStatus; +// this.medtronicHistoryData = medtronicHistoryData; +// this.rileyLinkServiceData = rileyLinkServiceData; +// this.serviceTaskExecutor = serviceTaskExecutor; + + } + + @Deprecated + public static OmnipodPumpPlugin getPlugin() { + if (plugin == null) + throw new IllegalStateException("Plugin not injected jet"); + return plugin; + } + + + @Override + protected void onStart() { + super.onStart(); + disposable.add(rxBus + .toObservable(EventPreferenceChange.class) + .observeOn(Schedulers.io()) + .subscribe(event -> { + if ((event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_basal_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_bolus_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_tbr_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_pod_debugging_options_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_beep_smb_enabled)) || + (event.isChanged(getResourceHelper(), R.string.key_omnipod_timechange_enabled))) + rileyLinkOmnipodService.verifyConfiguration(); + }, fabricPrivacy::logException) + ); + //rileyLinkOmnipodService.verifyConfiguration(); + //initPumpStatusData(); + } + +// @Override +// protected void onResume() { +// +// } + +// private void refreshConfiguration() { +// if (pumpStatusLocal != null) { +// pumpStatusLocal.refreshConfiguration(); +// } +// verifyConfiguration() +// } + + @Override + protected void onStop() { + disposable.clear(); + super.onStop(); + } + + private String getLogPrefix() { + return "OmnipodPlugin::"; + } + + + @Override + public void initPumpStatusData() { + + omnipodPumpStatus.lastConnection = sp.getLong(RileyLinkConst.Prefs.LastGoodDeviceCommunicationTime, 0L); + omnipodPumpStatus.lastDataTime = omnipodPumpStatus.lastConnection; + omnipodPumpStatus.previousConnection = omnipodPumpStatus.lastConnection; + + if (rileyLinkOmnipodService != null) rileyLinkOmnipodService.verifyConfiguration(); + + aapsLogger.debug(LTag.PUMP, "initPumpStatusData: " + this.omnipodPumpStatus); + + // set first Omnipod Pump Start + if (!sp.contains(OmnipodConst.Statistics.FirstPumpStart)) { + sp.putLong(OmnipodConst.Statistics.FirstPumpStart, System.currentTimeMillis()); + } + + } + + + @Override + public void onStartCustomActions() { + + // check status every minute (if any status needs refresh we send readStatus command) + new Thread(() -> { + + do { + SystemClock.sleep(60000); + + if (this.isInitialized) { + clearBusyQueue(); + } + + if (!this.omnipodStatusRequestList.isEmpty() || this.hasTimeDateOrTimeZoneChanged) { + if (!getCommandQueue().statusInQueue()) { + getCommandQueue().readStatus("Status Refresh Requested", null); + } + } + + doPodCheck(); + + } while (serviceRunning); + + }).start(); + } + + private void doPodCheck() { + if (System.currentTimeMillis() > this.nextPodCheck) { + if (omnipodUtil.getDriverState() == OmnipodDriverState.Initalized_NoPod) { + Notification notification = new Notification(Notification.OMNIPOD_POD_NOT_ATTACHED, resourceHelper.gs(R.string.omnipod_error_pod_not_attached), Notification.NORMAL); + rxBus.send(new EventNewNotification(notification)); + } else { + rxBus.send(new EventDismissNotification(Notification.OMNIPOD_POD_NOT_ATTACHED)); + } + + this.nextPodCheck = DateTimeUtil.getTimeInFutureFromMinutes(15); + } + } + + + @Override + public Class getServiceClass() { + return RileyLinkOmnipodService.class; + } + + @Override + public PumpStatus getPumpStatusData() { + return this.omnipodPumpStatus; + } + + + @Override + public String deviceID() { + return "Omnipod"; + } + + + // Pump Plugin + + private boolean isServiceSet() { + return rileyLinkOmnipodService != null; + } + + + @Override + public boolean isInitialized() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isInitialized"); + return isServiceSet() && isInitialized; + } + + + @Override + public boolean isBusy() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isBusy"); + + if (isServiceSet()) { + + if (isBusy || !omnipodPumpStatus.podAvailable) + return true; + + if (busyTimestamps.size() > 0) { + + clearBusyQueue(); + + return (busyTimestamps.size() > 0); + } + } + + return false; + } + + + @Override + public void resetRileyLinkConfiguration() { + rileyLinkOmnipodService.resetRileyLinkConfiguration(); + } + + + @Override + public boolean hasTuneUp() { + return false; + } + + + @Override + public void doTuneUpDevice() { + rileyLinkOmnipodService.doTuneUpDevice(); + } + + + @Override + public RileyLinkOmnipodService getRileyLinkService() { + return rileyLinkOmnipodService; + } + + + private synchronized void clearBusyQueue() { + + if (busyTimestamps.size() == 0) { + return; + } + + Set deleteFromQueue = new HashSet<>(); + + for (Long busyTimestamp : busyTimestamps) { + + if (System.currentTimeMillis() > busyTimestamp) { + deleteFromQueue.add(busyTimestamp); + } + } + + if (deleteFromQueue.size() == busyTimestamps.size()) { + busyTimestamps.clear(); + //setEnableCustomAction(MedtronicCustomActionType.ClearBolusBlock, false); + } + + if (deleteFromQueue.size() > 0) { + busyTimestamps.removeAll(deleteFromQueue); + } + + } + + + @Override + public boolean isConnected() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isConnected"); + return isServiceSet() && rileyLinkOmnipodService.isInitialized(); + } + + + @Override + public boolean isConnecting() { + if (displayConnectionMessages) + aapsLogger.debug(LTag.PUMP, getLogPrefix() + "isConnecting"); + return !isServiceSet() || !rileyLinkOmnipodService.isInitialized(); + } + + + @Override + public boolean isSuspended() { + + return (omnipodUtil.getDriverState() == OmnipodDriverState.Initalized_NoPod) || + (omnipodUtil.getPodSessionState() != null && omnipodUtil.getPodSessionState().isSuspended()); + +// return (pumpStatusLocal != null && !pumpStatusLocal.podAvailable) || +// (OmnipodUtil.getPodSessionState() != null && OmnipodUtil.getPodSessionState().isSuspended()); +// +// TODO ddd +// return (OmnipodUtil.getDriverState() == OmnipodDriverState.Initalized_NoPod) || +// (OmnipodUtil.getPodSessionState() != null && OmnipodUtil.getPodSessionState().isSuspended()); +// +// return (pumpStatusLocal != null && !pumpStatusLocal.podAvailable) || +// (OmnipodUtil.getPodSessionState() != null && OmnipodUtil.getPodSessionState().isSuspended()); + } + + @Override + public void getPumpStatus() { + + if (firstRun) { + initializePump(!isRefresh); + triggerUIChange(); + } else if (!omnipodStatusRequestList.isEmpty()) { + + List removeList = new ArrayList<>(); + + for (OmnipodStatusRequest omnipodStatusRequest : omnipodStatusRequestList) { + if (omnipodStatusRequest == OmnipodStatusRequest.GetPodPulseLog) { + OmnipodUITask omnipodUITask = omnipodUIComm.executeCommand(omnipodStatusRequest.getCommandType()); + + PodInfoRecentPulseLog result = (PodInfoRecentPulseLog) omnipodUITask.returnDataObject; + + if (result == null) { + aapsLogger.warn(LTag.PUMP, "Result was null."); + } else { + aapsLogger.warn(LTag.PUMP, "Result was NOT null."); + + Intent i = new Intent(MainApp.instance(), ErrorHelperActivity.class); + i.putExtra("soundid", 0); + i.putExtra("status", "Pulse Log (copied to clipboard):\n" + result.toString()); + i.putExtra("title", resourceHelper.gs(R.string.combo_warning)); + i.putExtra("clipboardContent", result.toString()); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + MainApp.instance().startActivity(i); + +// OKDialog.show(MainApp.instance().getApplicationContext(), MainApp.gs(R.string.action), +// "Pulse Log:\n" + result.toString(), null); + } + + } else { + omnipodUIComm.executeCommand(omnipodStatusRequest.getCommandType()); + } + removeList.add(omnipodStatusRequest); + } + + omnipodStatusRequestList.removeAll(removeList); + + } else if (this.hasTimeDateOrTimeZoneChanged) { + OmnipodUITask omnipodUITask = omnipodUIComm.executeCommand(OmnipodCommandType.SetTime); + + if (omnipodUITask.wasCommandSuccessful()) { + this.hasTimeDateOrTimeZoneChanged = false; + timeChangeRetries = 0; + + Notification notification = new Notification( + Notification.TIME_OR_TIMEZONE_CHANGE, + resourceHelper.gs(R.string.time_or_timezone_change), + Notification.INFO, 60); + rxBus.send(new EventNewNotification(notification)); + + } else { + timeChangeRetries++; + + if (timeChangeRetries > 3) { + this.hasTimeDateOrTimeZoneChanged = false; + timeChangeRetries = 0; + } + } + } + } + + + public void setIsBusy(boolean isBusy_) { + isBusy = isBusy_; + } + + + private void getPodPumpStatus() { + // TODO read pod status + aapsLogger.error(LTag.PUMP, "getPodPumpStatus() NOT IMPLEMENTED"); + + //addPodStatusRequest(OmnipodStatusRequest.GetPodState); + + //getPodPumpStatusObject().driverState = OmnipodDriverState.Initalized_PodAvailable; + //driverState = OmnipodDriverState.Initalized_PodAvailable; + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_PodAttached); + // we would probably need to read Basal Profile here too + } + + + List omnipodStatusRequestList = new ArrayList<>(); + + public void addPodStatusRequest(OmnipodStatusRequest pumpStatusRequest) { + if (pumpStatusRequest == OmnipodStatusRequest.ResetState) { + resetStatusState(); + } else { + omnipodStatusRequestList.add(pumpStatusRequest); + } + } + + @Override + public void setDriverState(OmnipodDriverState state) { + //this.driverState = state; + } + + + public void resetStatusState() { + firstRun = true; + isRefresh = true; + } + + + private void setRefreshButtonEnabled(boolean enabled) { + rxBus.send(new EventOmnipodRefreshButtonState(enabled)); + } + + + private void initializePump(boolean realInit) { + + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "initializePump - start"); + + // TODO ccc + //OmnipodPumpStatus podPumpStatus = getPodPumpStatusObject(); + + setRefreshButtonEnabled(false); + + PodSessionState podSessionState = null; + + if (omnipodUtil.getPodSessionState() != null) { + podSessionState = omnipodUtil.getPodSessionState(); + } else { + String podState = sp.getString(OmnipodConst.Prefs.PodState, null); + + aapsLogger.info(LTag.PUMP, "PodSessionState-SP: loaded from SharedPreferences: " + podState); + + if (podState != null) { + podSessionState = omnipodUtil.getGsonInstance().fromJson(podState, PodSessionState.class); + podSessionState.injectDaggerClass(injector); + omnipodUtil.setPodSessionState(podSessionState); + } + } + + + if (podSessionState != null) { + aapsLogger.debug(LTag.PUMP, "PodSessionState (saved): " + podSessionState); + + if (!isRefresh) { + pumpState = PumpDriverState.Initialized; + } + + // TODO handle if session state too old + getPodPumpStatus(); + + } else { + aapsLogger.debug(LTag.PUMP, "No PodSessionState found. Pod probably not running."); + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_NoPod); + } + + finishAction("Omnipod Pump"); + +// if (!sentIdToFirebase) { +// Bundle params = new Bundle(); +// params.putString("version", BuildConfig.VERSION); +// MainApp.getFirebaseAnalytics().logEvent("OmnipodPumpInit", params); +// +// sentIdToFirebase = true; +// } + + isInitialized = true; + + this.firstRun = false; + } + + + @Override + public boolean isThisProfileSet(Profile profile) { + + // TODO status was not yet read from pod + // TODO maybe not possible, need to see how we will handle that + if (currentProfile == null) { + this.currentProfile = profile; + return true; + } + + return (currentProfile.areProfileBasalPatternsSame(profile)); + } + + + @Override + public long lastDataTime() { + if (omnipodPumpStatus.lastConnection != 0) { + return omnipodPumpStatus.lastConnection; + } + + return System.currentTimeMillis(); + } + + + @Override + public double getBaseBasalRate() { + + if (currentProfile != null) { + int hour = (new GregorianCalendar()).get(Calendar.HOUR_OF_DAY); + return currentProfile.getBasalTimeFromMidnight(DateTimeUtil.getTimeInS(hour * 60)); + } else { + return 0.0d; + } + } + + + @Override + public double getReservoirLevel() { + return omnipodPumpStatus.reservoirRemainingUnits; + } + + + @Override + public int getBatteryLevel() { + return 75; + } + + + @Override + protected void triggerUIChange() { + rxBus.send(new EventOmnipodPumpValuesChanged()); + } + + + @Override + public boolean isFakingTempsByExtendedBoluses() { + return false; + } + + + @Override + @NonNull + protected PumpEnactResult deliverBolus(final DetailedBolusInfo detailedBolusInfo) { + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "deliverBolus - {}", detailedBolusInfo); + + setRefreshButtonEnabled(false); + + try { + + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.SetBolus, + detailedBolusInfo); + + PumpEnactResult result = responseTask.getResult(); + + setRefreshButtonEnabled(true); + + if (result.success) { + + // we subtract insulin, exact amount will be visible with next remainingInsulin update. +// if (getPodPumpStatusObject().reservoirRemainingUnits != 0 && +// getPodPumpStatusObject().reservoirRemainingUnits != 75 ) { +// getPodPumpStatusObject().reservoirRemainingUnits -= detailedBolusInfo.insulin; +// } + + incrementStatistics(detailedBolusInfo.isSMB ? OmnipodConst.Statistics.SMBBoluses + : OmnipodConst.Statistics.StandardBoluses); + + result.carbsDelivered(detailedBolusInfo.carbs); + } + + return result; + } finally { + finishAction("Bolus"); + } + } + + @Override + public void stopBolusDelivering() { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "stopBolusDelivering"); + + setRefreshButtonEnabled(false); + + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.CancelBolus); + + PumpEnactResult result = responseTask.getResult(); + + //setRefreshButtonEnabled(true); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "stopBolusDelivering - wasSuccess={}", result.success); + + //finishAction("Bolus"); + } + + + private void incrementStatistics(String statsKey) { + long currentCount = sp.getLong(statsKey, 0L); + currentCount++; + sp.putLong(statsKey, currentCount); + } + + + // if enforceNew===true current temp basal is canceled and new TBR set (duration is prolonged), + // if false and the same rate is requested enacted=false and success=true is returned and TBR is not changed + @Override + public PumpEnactResult setTempBasalAbsolute(Double absoluteRate, Integer durationInMinutes, Profile profile, + boolean enforceNew) { + + setRefreshButtonEnabled(false); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute: rate: {}, duration={}", absoluteRate, durationInMinutes); + + // read current TBR + TempBasalPair tbrCurrent = readTBR(); + + if (tbrCurrent != null) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute: Current Basal: duration: {} min, rate={}", + tbrCurrent.getDurationMinutes(), tbrCurrent.getInsulinRate()); + } + + if (tbrCurrent != null && !enforceNew) { + if (Round.isSame(tbrCurrent.getInsulinRate(), absoluteRate)) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute - No enforceNew and same rate. Exiting."); + finishAction("TBR"); + return new PumpEnactResult(getInjector()).success(true).enacted(false); + } + } + + // if TBR is running we will cancel it. +// if (tbrCurrent != null) { +// +// aapsLogger.info(LTag.PUMP,getLogPrefix() + "setTempBasalAbsolute - TBR running - so canceling it."); +// +// // CANCEL +// OmnipodUITask responseTask2 = omnipodUIComm.executeCommand(OmnipodCommandType.CancelTemporaryBasal); +// +// PumpEnactResult result = responseTask2.getResult(); +// +// if (result.success) { +// +// aapsLogger.info(LTag.PUMP,getLogPrefix() + "setTempBasalAbsolute - Current TBR cancelled."); +// } else { +// +// aapsLogger.error(LTag.PUMP,getLogPrefix() + "setTempBasalAbsolute - Cancel TBR failed."); +// +// finishAction("TBR"); +// +// return result; +// } +// } + + // now start new TBR + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.SetTemporaryBasal, + absoluteRate, durationInMinutes); + + PumpEnactResult result = responseTask.getResult(); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setTempBasalAbsolute - setTBR. Response: " + result.success); + + if (result.success) { + incrementStatistics(OmnipodConst.Statistics.TBRsSet); + } + + finishAction("TBR"); + return result; + } + + protected TempBasalPair readTBR() { + // TODO we can do it like this or read status from pod ?? + if (omnipodPumpStatus.tempBasalEnd < System.currentTimeMillis()) { + // TBR done + omnipodPumpStatus.clearTemporaryBasal(); + + return null; + } + + return omnipodPumpStatus.getTemporaryBasal(); + } + + + protected void finishAction(String overviewKey) { + if (overviewKey != null) + rxBus.send(new EventRefreshOverview(overviewKey, false)); + + triggerUIChange(); + + setRefreshButtonEnabled(true); + } + + + @Override + public PumpEnactResult cancelTempBasal(boolean enforceNew) { + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - started"); + + setRefreshButtonEnabled(false); + + TempBasalPair tbrCurrent = readTBR(); + + if (tbrCurrent == null) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - TBR already canceled."); + finishAction("TBR"); + return new PumpEnactResult(getInjector()).success(true).enacted(false); + } + + OmnipodUITask responseTask2 = omnipodUIComm.executeCommand(OmnipodCommandType.CancelTemporaryBasal); + + PumpEnactResult result = responseTask2.getResult(); + + finishAction("TBR"); + + if (result.success) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - Cancel TBR successful."); + + TemporaryBasal tempBasal = new TemporaryBasal() // + .date(System.currentTimeMillis()) // + .duration(0) // + .source(Source.USER); + + activePlugin.getActiveTreatments().addToHistoryTempBasal(tempBasal); + } else { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "cancelTempBasal - Cancel TBR failed."); + } + + return result; + } + + @NotNull + @Override + public String serialNumber() { + return StringUtils.isNotBlank(omnipodPumpStatus.podNumber) ? + omnipodPumpStatus.podNumber : "None"; + } + + @NotNull + @Override + public PumpEnactResult setNewBasalProfile(Profile profile) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "setNewBasalProfile"); + + // this shouldn't be needed, but let's do check if profile setting we are setting is same as current one + if (this.currentProfile != null && this.currentProfile.areProfileBasalPatternsSame(profile)) { + return new PumpEnactResult(getInjector()) // + .success(true) // + .enacted(false) // + .comment(resourceHelper.gs(R.string.medtronic_cmd_basal_profile_not_set_is_same)); + } + + setRefreshButtonEnabled(false); + + OmnipodUITask responseTask = omnipodUIComm.executeCommand(OmnipodCommandType.SetBasalProfile, + profile); + + PumpEnactResult result = responseTask.getResult(); + + aapsLogger.info(LTag.PUMP, getLogPrefix() + "Basal Profile was set: " + result.success); + + if (result.success) { + this.currentProfile = profile; + + Notification notification = new Notification(Notification.PROFILE_SET_OK, + resourceHelper.gs(R.string.profile_set_ok), + Notification.INFO, 60); + rxBus.send(new EventNewNotification(notification)); + } else { + Notification notification = new Notification(Notification.FAILED_UDPATE_PROFILE, + resourceHelper.gs(R.string.failedupdatebasalprofile), + Notification.URGENT); + rxBus.send(new EventNewNotification(notification)); + } + + return result; + } + + + // OPERATIONS not supported by Pump or Plugin + + protected List customActions = null; + + private CustomAction customActionResetRLConfig = new CustomAction( + R.string.medtronic_custom_action_reset_rileylink, OmnipodCustomActionType.ResetRileyLinkConfiguration, true); + + + @Override + public List getCustomActions() { + + if (customActions == null) { + this.customActions = Arrays.asList( + customActionResetRLConfig //, + ); + } + + return this.customActions; + } + + + @Override + public void executeCustomAction(CustomActionType customActionType) { + OmnipodCustomActionType mcat = (OmnipodCustomActionType) customActionType; + + switch (mcat) { + + case ResetRileyLinkConfiguration: { + serviceTaskExecutor.startTask(new ResetRileyLinkConfigurationTask(getInjector())); + } + break; + + default: + break; + } + } + + @Override + public void timezoneOrDSTChanged(TimeChangeType timeChangeType) { + aapsLogger.warn(LTag.PUMP, getLogPrefix() + "Time, Date and/or TimeZone changed. [changeType=" + timeChangeType.name() + ", eventHandlingEnabled=" + omnipodPumpStatus.timeChangeEventEnabled + "]"); + + if (omnipodUtil.getDriverState() == OmnipodDriverState.Initalized_PodAttached) { + if (omnipodPumpStatus.timeChangeEventEnabled) { + aapsLogger.info(LTag.PUMP, getLogPrefix() + "Time,and/or TimeZone changed event received and will be consumed by driver."); + this.hasTimeDateOrTimeZoneChanged = true; + } + } + } + + @Override + public boolean isUnreachableAlertTimeoutExceeded(long unreachableTimeoutMilliseconds) { + if (omnipodPumpStatus.lastConnection != 0 || omnipodPumpStatus.lastErrorConnection != 0) { + if (omnipodPumpStatus.lastConnection + unreachableTimeoutMilliseconds < System.currentTimeMillis()) { + if (omnipodPumpStatus.lastErrorConnection > omnipodPumpStatus.lastConnection) { + // We exceeded the alert threshold, and our last connection failed + // We should show an alert + return true; + } + + // Don't trigger an alert when we exceeded the thresholds, but the last communication was successful + // This happens when we simply didn't need to send any commands to the pump + return false; + } + } + + return false; + } + + + @Override + public RxBusWrapper getRxBus() { + return this.rxBus; + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/BolusProgressIndicationConsumer.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/BolusProgressIndicationConsumer.java new file mode 100644 index 0000000000..35d07e3622 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/BolusProgressIndicationConsumer.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +// TODO replace with Consumer when our min API level >= 24 +@FunctionalInterface +public interface BolusProgressIndicationConsumer { + void accept(double estimatedUnitsDelivered, int percentage); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationManager.java new file mode 100644 index 0000000000..f8ff351391 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationManager.java @@ -0,0 +1,345 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkCommunicationManager; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RFSpy; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RileyLinkCommunicationException; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.RLMessage; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RLMessageType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkBLEError; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.tasks.ServiceTaskExecutor; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.OmnipodPumpPlugin; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.OmnipodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommunicationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceOutOfSyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceResyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodReturnedErrorResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodPacket; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.DeactivatePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.ErrorResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +/** + * Created by andy on 6/29/18. + */ +public class OmnipodCommunicationManager extends RileyLinkCommunicationManager { + + @Inject public AAPSLogger aapsLogger; + @Inject OmnipodPumpStatus omnipodPumpStatus; + @Inject OmnipodPumpPlugin omnipodPumpPlugin; + @Inject RileyLinkServiceData rileyLinkServiceData; + @Inject ServiceTaskExecutor serviceTaskExecutor; + + public OmnipodCommunicationManager(HasAndroidInjector injector, RFSpy rfspy) { + super(injector, rfspy); + omnipodPumpStatus.previousConnection = sp.getLong( + RileyLinkConst.Prefs.LastGoodDeviceCommunicationTime, 0L); + } + +// @Override +// protected void configurePumpSpecificSettings() { +// } + + @Override + public boolean tryToConnectToDevice() { + // TODO + return false; + } + + @Override + public byte[] createPumpMessageContent(RLMessageType type) { + return new byte[0]; + } + + @Override public PumpStatus getPumpStatus() { + return null; + } + + @Override public boolean isDeviceReachable() { + return false; + } + + @Override + public boolean hasTunning() { + return false; + } + + @Override + public E createResponseMessage(byte[] payload, Class clazz) { + return (E) new OmnipodPacket(payload); + } + + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.omnipodPumpStatus.setPumpDeviceState(pumpDeviceState); + } + + public T sendCommand(Class responseClass, PodState podState, MessageBlock command) { + return sendCommand(responseClass, podState, command, true); + } + + public T sendCommand(Class responseClass, PodState podState, MessageBlock command, boolean automaticallyResyncNone) { + OmnipodMessage message = new OmnipodMessage(podState.getAddress(), Collections.singletonList(command), podState.getMessageNumber()); + return exchangeMessages(responseClass, podState, message, automaticallyResyncNone); + } + + // Convenience method + public T executeAction(OmnipodAction action) { + return action.execute(this); + } + + public T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message) { + return exchangeMessages(responseClass, podState, message, true); + } + + public T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message, boolean automaticallyResyncNonce) { + return exchangeMessages(responseClass, podState, message, null, null, automaticallyResyncNonce); + } + + public synchronized T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride) { + return exchangeMessages(responseClass, podState, message, addressOverride, ackAddressOverride, true); + } + + public synchronized T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride, boolean automaticallyResyncNonce) { + + aapsLogger.debug(LTag.PUMPCOMM, "Exchanging OmnipodMessage [responseClass={}, podState={}, message={}, addressOverride={}, ackAddressOverride={}, automaticallyResyncNonce={}]: {}", // + responseClass.getSimpleName(), podState, message, addressOverride, ackAddressOverride, automaticallyResyncNonce, message); + + for (int i = 0; 2 > i; i++) { + + if (podState.hasNonceState() && message.isNonceResyncable()) { + podState.advanceToNextNonce(); + } + + MessageBlock responseMessageBlock = transportMessages(podState, message, addressOverride, ackAddressOverride); + + if (responseMessageBlock instanceof StatusResponse) { + podState.updateFromStatusResponse((StatusResponse) responseMessageBlock); + } + + if (responseClass.isInstance(responseMessageBlock)) { + return (T) responseMessageBlock; + } else { + if (responseMessageBlock.getType() == MessageBlockType.ERROR_RESPONSE) { + ErrorResponse error = (ErrorResponse) responseMessageBlock; + if (error.getErrorResponseType() == ErrorResponseType.BAD_NONCE) { + podState.resyncNonce(error.getNonceSearchKey(), message.getSentNonce(), message.getSequenceNumber()); + if (automaticallyResyncNonce) { + message.resyncNonce(podState.getCurrentNonce()); + } else { + throw new NonceOutOfSyncException(); + } + } else { + throw new PodReturnedErrorResponseException((ErrorResponse) responseMessageBlock); + } + } else if (responseMessageBlock.getType() == MessageBlockType.POD_INFO_RESPONSE && ((PodInfoResponse) responseMessageBlock).getSubType() == PodInfoType.FAULT_EVENT) { + PodInfoFaultEvent faultEvent = ((PodInfoResponse) responseMessageBlock).getPodInfo(); + podState.setFaultEvent(faultEvent); + throw new PodFaultException(faultEvent); + } else { + throw new IllegalResponseException(responseClass.getSimpleName(), responseMessageBlock.getType()); + } + } + } + + throw new NonceResyncException(); + } + + private MessageBlock transportMessages(PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride) { + int packetAddress = podState.getAddress(); + if (addressOverride != null) { + packetAddress = addressOverride; + } + + boolean firstPacket = true; + byte[] encodedMessage; + // this does not work well with the deactivate pod command, we somehow either + // receive an ACK instead of a normal response, or a partial response and a communication timeout + if (message.isNonceResyncable() && !message.containsBlock(DeactivatePodCommand.class)) { + OmnipodMessage paddedMessage = new OmnipodMessage(message); + // If messages are nonce resyncable, we want do distinguish between certain and uncertain failures for verification purposes + // However, some commands (e.g. cancel delivery) are single packet command by nature. When we get a timeout with a single packet, + // we are unsure whether or not the command was received by the pod + // However, if we send > 1 packet, we know that the command wasn't received if we never send the subsequent packets, + // because the last packet contains the CRC. + // So we pad the message with get status commands to make it > packet + paddedMessage.padWithGetStatusCommands(PacketType.PDM.getMaxBodyLength()); // First packet is of type PDM + encodedMessage = paddedMessage.getEncoded(); + } else { + encodedMessage = message.getEncoded(); + } + + OmnipodPacket response = null; + while (encodedMessage.length > 0) { + PacketType packetType = firstPacket ? PacketType.PDM : PacketType.CON; + OmnipodPacket packet = new OmnipodPacket(packetAddress, packetType, podState.getPacketNumber(), encodedMessage); + byte[] encodedMessageInPacket = packet.getEncodedMessage(); + + // getting the data remaining to be sent + encodedMessage = ByteUtil.substring(encodedMessage, encodedMessageInPacket.length, encodedMessage.length - encodedMessageInPacket.length); + firstPacket = false; + + try { + // We actually ignore previous (ack) responses if it was not last packet to send + response = exchangePackets(podState, packet); + } catch (Exception ex) { + OmnipodException newException; + if (ex instanceof OmnipodException) { + newException = (OmnipodException) ex; + } else { + newException = new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex); + } + + boolean lastPacket = encodedMessage.length == 0; + + // If this is not the last packet, the message wasn't fully sent, + // so it's impossible for the pod to have received the message + newException.setCertainFailure(!lastPacket); + + aapsLogger.debug(LTag.PUMPCOMM, "Caught exception in transportMessages. Set certainFailure to {} because encodedMessage.length={}", newException.isCertainFailure(), encodedMessage.length); + + throw newException; + } + } + + if (response.getPacketType() == PacketType.ACK) { + podState.increasePacketNumber(1); + throw new IllegalPacketTypeException(null, PacketType.ACK); + } + + OmnipodMessage receivedMessage = null; + byte[] receivedMessageData = response.getEncodedMessage(); + while (receivedMessage == null) { + try { + receivedMessage = OmnipodMessage.decodeMessage(receivedMessageData); + } catch (NotEnoughDataException ex) { + // Message is (probably) not complete yet + OmnipodPacket ackForCon = createAckPacket(podState, packetAddress, ackAddressOverride); + + try { + OmnipodPacket conPacket = exchangePackets(podState, ackForCon, 3, 40); + if (conPacket.getPacketType() != PacketType.CON) { + throw new IllegalPacketTypeException(PacketType.CON, conPacket.getPacketType()); + } + receivedMessageData = ByteUtil.concat(receivedMessageData, conPacket.getEncodedMessage()); + } catch (OmnipodException ex2) { + throw ex2; + } catch (Exception ex2) { + throw new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex2); + } + + } + } + + podState.increaseMessageNumber(2); + + ackUntilQuiet(podState, packetAddress, ackAddressOverride); + + List messageBlocks = receivedMessage.getMessageBlocks(); + + if (messageBlocks.size() == 0) { + throw new NotEnoughDataException(receivedMessageData); + } else if (messageBlocks.size() > 1) { + // BS: don't expect this to happen + aapsLogger.error(LTag.PUMPBTCOMM, "Received more than one message block: {}", messageBlocks.toString()); + } + + return messageBlocks.get(0); + } + + private OmnipodPacket createAckPacket(PodState podState, Integer packetAddress, Integer messageAddress) { + int pktAddress = podState.getAddress(); + int msgAddress = podState.getAddress(); + if (packetAddress != null) { + pktAddress = packetAddress; + } + if (messageAddress != null) { + msgAddress = messageAddress; + } + return new OmnipodPacket(pktAddress, PacketType.ACK, podState.getPacketNumber(), ByteUtil.getBytesFromInt(msgAddress)); + } + + private void ackUntilQuiet(PodState podState, Integer packetAddress, Integer messageAddress) { + OmnipodPacket ack = createAckPacket(podState, packetAddress, messageAddress); + boolean quiet = false; + while (!quiet) try { + sendAndListen(ack, 300, 1, 0, 40, OmnipodPacket.class); + } catch (RileyLinkCommunicationException ex) { + if (RileyLinkBLEError.Timeout.equals(ex.getErrorCode())) { + quiet = true; + } else { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in ackUntilQuiet", ex); + } + } catch (OmnipodException ex) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in ackUntilQuiet", ex); + } catch (Exception ex) { + throw new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex); + } + + podState.increasePacketNumber(1); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet) { + return exchangePackets(podState, packet, 0, 333, 9000, 127); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet, int repeatCount, int preambleExtensionMilliseconds) { + return exchangePackets(podState, packet, repeatCount, 333, 9000, preambleExtensionMilliseconds); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet, int repeatCount, int responseTimeoutMilliseconds, int exchangeTimeoutMilliseconds, int preambleExtensionMilliseconds) { + long timeoutTime = System.currentTimeMillis() + exchangeTimeoutMilliseconds; + + while (System.currentTimeMillis() < timeoutTime) { + OmnipodPacket response = null; + try { + response = sendAndListen(packet, responseTimeoutMilliseconds, repeatCount, 9, preambleExtensionMilliseconds, OmnipodPacket.class); + } catch (RileyLinkCommunicationException | OmnipodException ex) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in exchangePackets", ex); + } catch (Exception ex) { + throw new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, ex); + } + if (response == null || !response.isValid()) { + continue; + } + if (response.getAddress() != packet.getAddress()) { + continue; + } + if (response.getSequenceNumber() != ((podState.getPacketNumber() + 1) & 0b11111)) { + continue; + } + + podState.increasePacketNumber(2); + return response; + } + throw new CommunicationException(CommunicationException.Type.TIMEOUT); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodManager.java new file mode 100644 index 0000000000..56c8de7626 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodManager.java @@ -0,0 +1,664 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; + +import java.util.EnumSet; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.AcknowledgeAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.AssignAddressAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.CancelDeliveryAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigurePodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.DeactivatePodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.GetPodInfoAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.GetStatusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.InsertCannulaAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.PrimeAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetBasalScheduleAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetTempBasalAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.InsertCannulaService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PrimeService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommunicationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalDeliveryStatusException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceOutOfSyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.CancelDeliveryCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodStateChangedHandler; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.utils.sharedPreferences.SP; +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.SingleSubject; + +public class OmnipodManager { + private static final int ACTION_VERIFICATION_TRIES = 3; + + protected final OmnipodCommunicationManager communicationService; + private final PodStateChangedHandler podStateChangedHandler; + protected PodSessionState podState; + + private ActiveBolusData activeBolusData; + private final Object bolusDataMutex = new Object(); + + //private HasAndroidInjector injector; + AAPSLogger aapsLogger; + SP sp; + + public OmnipodManager(//HasAndroidInjector injector, + AAPSLogger aapsLogger, + SP sp, + OmnipodCommunicationManager communicationService, + PodSessionState podState, + PodStateChangedHandler podStateChangedHandler) { +// this.injector = injector; +// this.injector.androidInjector().inject(this); + if (communicationService == null) { + throw new IllegalArgumentException("Communication service cannot be null"); + } + this.aapsLogger = aapsLogger; + this.sp = sp; + this.communicationService = communicationService; + if (podState != null) { + podState.setStateChangedHandler(podStateChangedHandler); + } + this.podState = podState; + this.podStateChangedHandler = podStateChangedHandler; + } + +// public OmnipodManager(HasAndroidInjector injector, +// OmnipodCommunicationManager communicationService, +// PodSessionState podState) { +// this(injector, communicationService, podState, null); +// } + + public synchronized Single pairAndPrime() { + logStartingCommandExecution("pairAndPrime"); + + try { + if (podState == null) { + podState = communicationService.executeAction( + new AssignAddressAction(podStateChangedHandler)); + } else if (SetupProgress.PRIMING.isBefore(podState.getSetupProgress())) { + throw new IllegalSetupProgressException(SetupProgress.ADDRESS_ASSIGNED, podState.getSetupProgress()); + } + + if (SetupProgress.ADDRESS_ASSIGNED.equals(podState.getSetupProgress())) { + communicationService.executeAction(new ConfigurePodAction(podState)); + } + + communicationService.executeAction(new PrimeAction(new PrimeService(), podState)); + } finally { + logCommandExecutionFinished("pairAndPrime"); + } + + long delayInSeconds = calculateBolusDuration(OmnipodConst.POD_PRIME_BOLUS_UNITS, OmnipodConst.POD_PRIMING_DELIVERY_RATE).getStandardSeconds(); + + return Single.timer(delayInSeconds, TimeUnit.SECONDS) // + .map(o -> verifySetupAction(statusResponse -> + PrimeAction.updatePrimingStatus(podState, statusResponse, aapsLogger), SetupProgress.PRIMING_FINISHED)) // + .observeOn(Schedulers.io()); + } + + public synchronized Single insertCannula(BasalSchedule basalSchedule) { + if (podState == null || podState.getSetupProgress().isBefore(SetupProgress.PRIMING_FINISHED)) { + throw new IllegalSetupProgressException(SetupProgress.PRIMING_FINISHED, podState == null ? null : podState.getSetupProgress()); + } else if (podState.getSetupProgress().isAfter(SetupProgress.CANNULA_INSERTING)) { + throw new IllegalSetupProgressException(SetupProgress.CANNULA_INSERTING, podState.getSetupProgress()); + } + + logStartingCommandExecution("insertCannula [basalSchedule=" + basalSchedule + "]"); + + try { + communicationService.executeAction(new InsertCannulaAction(new InsertCannulaService(), podState, basalSchedule)); + } finally { + logCommandExecutionFinished("insertCannula"); + } + + long delayInSeconds = calculateBolusDuration(OmnipodConst.POD_CANNULA_INSERTION_BOLUS_UNITS, OmnipodConst.POD_CANNULA_INSERTION_DELIVERY_RATE).getStandardSeconds(); + + return Single.timer(delayInSeconds, TimeUnit.SECONDS) // + .map(o -> verifySetupAction(statusResponse -> + InsertCannulaAction.updateCannulaInsertionStatus(podState, statusResponse, aapsLogger), SetupProgress.COMPLETED)) // + .observeOn(Schedulers.io()); + } + + public synchronized StatusResponse getPodStatus() { + if (podState == null) { + throw new IllegalSetupProgressException(SetupProgress.PRIMING_FINISHED, null); + } + + logStartingCommandExecution("getPodStatus"); + + try { + return communicationService.executeAction(new GetStatusAction(podState)); + } finally { + logCommandExecutionFinished("getPodStatus"); + } + } + + public synchronized PodInfoResponse getPodInfo(PodInfoType podInfoType) { + assertReadyForDelivery(); + + logStartingCommandExecution("getPodInfo"); + + try { + return communicationService.executeAction(new GetPodInfoAction(podState, podInfoType)); + } finally { + logCommandExecutionFinished("getPodInfo"); + } + } + + public synchronized StatusResponse acknowledgeAlerts() { + assertReadyForDelivery(); + + logStartingCommandExecution("acknowledgeAlerts"); + + try { + return executeAndVerify(() -> communicationService.executeAction(new AcknowledgeAlertsAction(podState, podState.getActiveAlerts()))); + } finally { + logCommandExecutionFinished("acknowledgeAlerts"); + } + } + + // CAUTION: cancels all delivery + // CAUTION: suspends and then resumes delivery. An OmnipodException[certainFailure=false] indicates that the pod is or might be suspended + public synchronized StatusResponse setBasalSchedule(BasalSchedule schedule, boolean acknowledgementBeep) { + assertReadyForDelivery(); + + logStartingCommandExecution("setBasalSchedule [basalSchedule=" + schedule + ", acknowledgementBeep=" + acknowledgementBeep + "]"); + + try { + cancelDelivery(EnumSet.allOf(DeliveryType.class), acknowledgementBeep); + } catch (Exception ex) { + logCommandExecutionFinished("setBasalSchedule"); + throw ex; + } + + try { + try { + return executeAndVerify(() -> communicationService.executeAction(new SetBasalScheduleAction(podState, schedule, + false, podState.getScheduleOffset(), acknowledgementBeep))); + } catch (OmnipodException ex) { + // Treat all exceptions as uncertain failures, because all delivery has been suspended here. + // Setting this to an uncertain failure will enable for the user to get an appropriate warning + ex.setCertainFailure(false); + throw ex; + } + } finally { + logCommandExecutionFinished("setBasalSchedule"); + } + } + + // CAUTION: cancels temp basal and then sets new temp basal. An OmnipodException[certainFailure=false] indicates that the pod might have cancelled the previous temp basal, but did not set a new temp basal + public synchronized StatusResponse setTemporaryBasal(double rate, Duration duration, boolean acknowledgementBeep, boolean completionBeep) { + assertReadyForDelivery(); + + logStartingCommandExecution("setTemporaryBasal [rate=" + rate + ", duration=" + duration + ", acknowledgementBeep=" + acknowledgementBeep + ", completionBeep=" + completionBeep + "]"); + + try { + cancelDelivery(EnumSet.of(DeliveryType.TEMP_BASAL), acknowledgementBeep); + } catch (Exception ex) { + logCommandExecutionFinished("setTemporaryBasal"); + throw ex; + } + + try { + return executeAndVerify(() -> communicationService.executeAction(new SetTempBasalAction( + podState, rate, duration, + acknowledgementBeep, completionBeep))); + } catch (OmnipodException ex) { + // Treat all exceptions as uncertain failures, because all delivery has been suspended here. + // Setting this to an uncertain failure will enable for the user to get an appropriate warning + ex.setCertainFailure(false); + throw ex; + } finally { + logCommandExecutionFinished("setTemporaryBasal"); + } + } + + public synchronized void cancelTemporaryBasal(boolean acknowledgementBeep) { + cancelDelivery(EnumSet.of(DeliveryType.TEMP_BASAL), acknowledgementBeep); + } + + private synchronized StatusResponse cancelDelivery(EnumSet deliveryTypes, boolean acknowledgementBeep) { + assertReadyForDelivery(); + + logStartingCommandExecution("cancelDelivery [deliveryTypes=" + deliveryTypes + ", acknowledgementBeep=" + acknowledgementBeep + "]"); + + try { + return executeAndVerify(() -> { + StatusResponse statusResponse = communicationService.executeAction(new CancelDeliveryAction(podState, deliveryTypes, acknowledgementBeep)); + aapsLogger.info(LTag.PUMPBTCOMM, "Status response after cancel delivery[types={}]: {}", deliveryTypes.toString(), statusResponse.toString()); + return statusResponse; + }); + } finally { + logCommandExecutionFinished("cancelDelivery"); + } + } + + // Returns a SingleSubject that returns when the bolus has finished. + // When a bolus is cancelled, it will return after cancellation and report the estimated units delivered + // Only throws OmnipodException[certainFailure=false] + public synchronized BolusCommandResult bolus(Double units, boolean acknowledgementBeep, boolean completionBeep, BolusProgressIndicationConsumer progressIndicationConsumer) { + assertReadyForDelivery(); + + logStartingCommandExecution("bolus [units=" + units + ", acknowledgementBeep=" + acknowledgementBeep + ", completionBeep=" + completionBeep + "]"); + + CommandDeliveryStatus commandDeliveryStatus = CommandDeliveryStatus.SUCCESS; + + try { + executeAndVerify(() -> communicationService.executeAction(new BolusAction(podState, units, acknowledgementBeep, completionBeep))); + } catch (OmnipodException ex) { + if (ex.isCertainFailure()) { + throw ex; + } + + // Catch uncertain exceptions as we still want to report bolus progress indication + aapsLogger.error(LTag.PUMPBTCOMM, "Caught exception[certainFailure=false] in bolus", ex); + commandDeliveryStatus = CommandDeliveryStatus.UNCERTAIN_FAILURE; + } finally { + logCommandExecutionFinished("bolus"); + } + + DateTime startDate = DateTime.now().minus(OmnipodConst.AVERAGE_BOLUS_COMMAND_COMMUNICATION_DURATION); + + CompositeDisposable disposables = new CompositeDisposable(); + Duration bolusDuration = calculateBolusDuration(units, OmnipodConst.POD_BOLUS_DELIVERY_RATE); + Duration estimatedRemainingBolusDuration = bolusDuration.minus(OmnipodConst.AVERAGE_BOLUS_COMMAND_COMMUNICATION_DURATION); + + if (progressIndicationConsumer != null) { + int numberOfProgressReports = Math.max(20, Math.min(100, (int) Math.ceil(units) * 10)); + long progressReportInterval = estimatedRemainingBolusDuration.getMillis() / numberOfProgressReports; + + disposables.add(Flowable.intervalRange(0, numberOfProgressReports + 1, 0, progressReportInterval, TimeUnit.MILLISECONDS) // + .observeOn(Schedulers.io()) // + .subscribe(count -> { + int percentage = (int) ((double) count / numberOfProgressReports * 100); + double estimatedUnitsDelivered = activeBolusData == null ? 0 : activeBolusData.estimateUnitsDelivered(); + progressIndicationConsumer.accept(estimatedUnitsDelivered, percentage); + })); + } + + SingleSubject bolusCompletionSubject = SingleSubject.create(); + + synchronized (bolusDataMutex) { + activeBolusData = new ActiveBolusData(units, startDate, bolusCompletionSubject, disposables); + } + + disposables.add(Completable.complete() // + .delay(estimatedRemainingBolusDuration.getMillis() + 250, TimeUnit.MILLISECONDS) // + .observeOn(Schedulers.io()) // + .doOnComplete(() -> { + synchronized (bolusDataMutex) { + double unitsNotDelivered = 0.0d; + + for (int i = 0; i < ACTION_VERIFICATION_TRIES; i++) { + try { + // Retrieve a status response in order to update the pod state + StatusResponse statusResponse = getPodStatus(); + if (statusResponse.getDeliveryStatus().isBolusing()) { + throw new IllegalDeliveryStatusException(DeliveryStatus.NORMAL, statusResponse.getDeliveryStatus()); + } else { + break; + } + } catch (PodFaultException ex) { + // Substract units not delivered in case of a Pod failure + unitsNotDelivered = ex.getFaultEvent().getInsulinNotDelivered(); + + aapsLogger.debug(LTag.PUMPBTCOMM, "Caught PodFaultException in bolus completion verification", ex); + break; + } catch (Exception ex) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Ignoring exception in bolus completion verification", ex); + } + } + + if (hasActiveBolus()) { + activeBolusData.bolusCompletionSubject.onSuccess(new BolusDeliveryResult(units - unitsNotDelivered)); + activeBolusData = null; + } + } + }) + .subscribe()); + + return new BolusCommandResult(commandDeliveryStatus, bolusCompletionSubject); + } + + public synchronized void cancelBolus(boolean acknowledgementBeep) { + assertReadyForDelivery(); + + synchronized (bolusDataMutex) { + if (activeBolusData == null) { + throw new IllegalDeliveryStatusException(DeliveryStatus.BOLUS_IN_PROGRESS, podState.getLastDeliveryStatus()); + } + + logStartingCommandExecution("cancelBolus [acknowledgementBeep=" + acknowledgementBeep + "]"); + + try { + StatusResponse statusResponse = cancelDelivery(EnumSet.of(DeliveryType.BOLUS), acknowledgementBeep); + discardActiveBolusData(statusResponse.getInsulinNotDelivered()); + } catch (PodFaultException ex) { + discardActiveBolusData(ex.getFaultEvent().getInsulinNotDelivered()); + throw ex; + } finally { + logCommandExecutionFinished("cancelBolus"); + } + } + } + + private void discardActiveBolusData(double unitsNotDelivered) { + synchronized (bolusDataMutex) { + activeBolusData.getDisposables().dispose(); + activeBolusData.getBolusCompletionSubject().onSuccess(new BolusDeliveryResult(activeBolusData.getUnits() - unitsNotDelivered)); + activeBolusData = null; + } + } + + public synchronized void suspendDelivery(boolean acknowledgementBeep) { + cancelDelivery(EnumSet.allOf(DeliveryType.class), acknowledgementBeep); + } + + // Same as setting basal schedule, but without suspending delivery first + public synchronized StatusResponse resumeDelivery(boolean acknowledgementBeep) { + assertReadyForDelivery(); + logStartingCommandExecution("resumeDelivery"); + + try { + return executeAndVerify(() -> communicationService.executeAction(new SetBasalScheduleAction(podState, podState.getBasalSchedule(), + false, podState.getScheduleOffset(), acknowledgementBeep))); + } finally { + logCommandExecutionFinished("resumeDelivery"); + } + } + + // CAUTION: cancels all delivery + // CAUTION: suspends and then resumes delivery. An OmnipodException[certainFailure=false] indicates that the pod is or might be suspended + public synchronized void setTime(boolean acknowledgementBeeps) { + assertReadyForDelivery(); + + logStartingCommandExecution("setTime [acknowledgementBeeps=" + acknowledgementBeeps + "]"); + + try { + cancelDelivery(EnumSet.allOf(DeliveryType.class), acknowledgementBeeps); + } catch (Exception ex) { + logCommandExecutionFinished("setTime"); + throw ex; + } + + DateTimeZone oldTimeZone = podState.getTimeZone(); + + try { + // Joda seems to cache the default time zone, so we use the JVM's + DateTimeZone.setDefault(DateTimeZone.forTimeZone(TimeZone.getDefault())); + podState.setTimeZone(DateTimeZone.getDefault()); + + setBasalSchedule(podState.getBasalSchedule(), acknowledgementBeeps); + } catch (OmnipodException ex) { + // Treat all exceptions as uncertain failures, because all delivery has been suspended here. + // Setting this to an uncertain failure will enable for the user to get an appropriate warning + podState.setTimeZone(oldTimeZone); + ex.setCertainFailure(false); + throw ex; + } finally { + logCommandExecutionFinished("setTime"); + } + } + + public synchronized void deactivatePod() { + if (podState == null) { + throw new IllegalSetupProgressException(SetupProgress.ADDRESS_ASSIGNED, null); + } + + logStartingCommandExecution("deactivatePod"); + + // Try to get pulse log for diagnostics + // FIXME replace by storing to file + try { + PodInfoResponse podInfoResponse = communicationService.executeAction(new GetPodInfoAction(podState, PodInfoType.RECENT_PULSE_LOG)); + PodInfoRecentPulseLog pulseLogInfo = podInfoResponse.getPodInfo(); + aapsLogger.info(LTag.PUMPBTCOMM, "Retrieved pulse log from the pod: {}", pulseLogInfo.toString()); + } catch (Exception ex) { + aapsLogger.warn(LTag.PUMPBTCOMM, "Failed to retrieve pulse log from the pod", ex); + } + + try { + // Always send acknowledgement beeps here. Matches the PDM's behavior + communicationService.executeAction(new DeactivatePodAction(podState, true)); + } catch (PodFaultException ex) { + aapsLogger.info(LTag.PUMPBTCOMM, "Ignoring PodFaultException in deactivatePod", ex); + } finally { + logCommandExecutionFinished("deactivatePod"); + } + + resetPodState(false); + } + + public void resetPodState(boolean forcedByUser) { + aapsLogger.warn(LTag.PUMPBTCOMM, "resetPodState has been called. forcedByUser={}", forcedByUser); + podState = null; + sp.remove(OmnipodConst.Prefs.PodState); + } + + public OmnipodCommunicationManager getCommunicationService() { + return communicationService; + } + + public DateTime getTime() { + return podState.getTime(); + } + + public boolean isReadyForDelivery() { + return podState != null && podState.getSetupProgress() == SetupProgress.COMPLETED; + } + + public boolean hasActiveBolus() { + synchronized (bolusDataMutex) { + return activeBolusData != null; + } + } + + // FIXME this is dirty, we should not expose the original pod state + public PodSessionState getPodState() { + return this.podState; + } + + public String getPodStateAsString() { + return podState == null ? "null" : podState.toString(); + } + + // Only works for commands with nonce resyncable message blocks + // FIXME method is too big, needs refactoring + private StatusResponse executeAndVerify(VerifiableAction runnable) { + try { + return runnable.run(); + } catch (Exception originalException) { + if (isCertainFailure(originalException)) { + throw originalException; + } else { + aapsLogger.warn(LTag.PUMPBTCOMM, "Caught exception in executeAndVerify. Verifying command by using cancel none command to verify nonce", originalException); + + try { + logStartingCommandExecution("verifyCommand"); + StatusResponse statusResponse = communicationService.sendCommand(StatusResponse.class, podState, + new CancelDeliveryCommand(podState.getCurrentNonce(), BeepType.NO_BEEP, DeliveryType.NONE), false); + aapsLogger.info(LTag.PUMPBTCOMM, "Command status resolved to SUCCESS. Status response after cancelDelivery[types=DeliveryType.NONE]: {}", statusResponse); + + return statusResponse; + } catch (NonceOutOfSyncException verificationException) { + aapsLogger.error(LTag.PUMPBTCOMM, "Command resolved to FAILURE (CERTAIN_FAILURE)", verificationException); + + if (originalException instanceof OmnipodException) { + ((OmnipodException) originalException).setCertainFailure(true); + throw originalException; + } else { + OmnipodException newException = new CommunicationException(CommunicationException.Type.UNEXPECTED_EXCEPTION, originalException); + newException.setCertainFailure(true); + throw newException; + } + } catch (Exception verificationException) { + aapsLogger.error(LTag.PUMPBTCOMM, "Command unresolved (UNCERTAIN_FAILURE)", verificationException); + throw originalException; + } finally { + logCommandExecutionFinished("verifyCommand"); + } + } + } + } + + private void assertReadyForDelivery() { + if (!isReadyForDelivery()) { + throw new IllegalSetupProgressException(SetupProgress.COMPLETED, podState == null ? null : podState.getSetupProgress()); + } + } + + private SetupActionResult verifySetupAction(StatusResponseConsumer setupActionResponseHandler, SetupProgress expectedSetupProgress) { + SetupActionResult result = null; + for (int i = 0; ACTION_VERIFICATION_TRIES > i; i++) { + try { + StatusResponse delayedStatusResponse = communicationService.executeAction(new GetStatusAction(podState)); + setupActionResponseHandler.accept(delayedStatusResponse); + + if (podState.getSetupProgress().equals(expectedSetupProgress)) { + result = new SetupActionResult(SetupActionResult.ResultType.SUCCESS); + break; + } else { + result = new SetupActionResult(SetupActionResult.ResultType.FAILURE) // + .setupProgress(podState.getSetupProgress()); + break; + } + } catch (Exception ex) { + result = new SetupActionResult(SetupActionResult.ResultType.VERIFICATION_FAILURE) // + .exception(ex); + } + } + return result; + } + + private void logStartingCommandExecution(String action) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Starting command execution for action: " + action); + } + + private void logCommandExecutionFinished(String action) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Command execution finished for action: " + action); + } + + private static Duration calculateBolusDuration(double units, double deliveryRate) { + return Duration.standardSeconds((long) Math.ceil(units / deliveryRate)); + } + + public static Duration calculateBolusDuration(double units) { + return calculateBolusDuration(units, OmnipodConst.POD_BOLUS_DELIVERY_RATE); + } + + public static boolean isCertainFailure(Exception ex) { + return ex instanceof OmnipodException && ((OmnipodException) ex).isCertainFailure(); + } + + public static class BolusCommandResult { + private final CommandDeliveryStatus commandDeliveryStatus; + private final SingleSubject deliveryResultSubject; + + public BolusCommandResult(CommandDeliveryStatus commandDeliveryStatus, SingleSubject deliveryResultSubject) { + this.commandDeliveryStatus = commandDeliveryStatus; + this.deliveryResultSubject = deliveryResultSubject; + } + + public CommandDeliveryStatus getCommandDeliveryStatus() { + return commandDeliveryStatus; + } + + public SingleSubject getDeliveryResultSubject() { + return deliveryResultSubject; + } + } + + public static class BolusDeliveryResult { + private final double unitsDelivered; + + public BolusDeliveryResult(double unitsDelivered) { + this.unitsDelivered = unitsDelivered; + } + + public double getUnitsDelivered() { + return unitsDelivered; + } + } + + public enum CommandDeliveryStatus { + SUCCESS, + CERTAIN_FAILURE, + UNCERTAIN_FAILURE + } + + // TODO replace with Consumer when our min API level >= 24 + @FunctionalInterface + private interface StatusResponseConsumer { + void accept(StatusResponse statusResponse); + } + + private static class ActiveBolusData { + private final double units; + private volatile DateTime startDate; + private volatile SingleSubject bolusCompletionSubject; + private volatile CompositeDisposable disposables; + + private ActiveBolusData(double units, DateTime startDate, SingleSubject bolusCompletionSubject, CompositeDisposable disposables) { + this.units = units; + this.startDate = startDate; + this.bolusCompletionSubject = bolusCompletionSubject; + this.disposables = disposables; + } + + public double getUnits() { + return units; + } + + public DateTime getStartDate() { + return startDate; + } + + public CompositeDisposable getDisposables() { + return disposables; + } + + public SingleSubject getBolusCompletionSubject() { + return bolusCompletionSubject; + } + + public double estimateUnitsDelivered() { + long elapsedMillis = new Duration(startDate, DateTime.now()).getMillis(); + long totalDurationMillis = (long) (units / OmnipodConst.POD_BOLUS_DELIVERY_RATE * 1000); + double factor = (double) elapsedMillis / totalDurationMillis; + double estimatedUnits = Math.min(1D, factor) * units; + + int roundingDivisor = (int) (1 / OmnipodConst.POD_PULSE_SIZE); + return (double) Math.round(estimatedUnits * roundingDivisor) / roundingDivisor; + } + } + + // Could be replaced with Supplier when min API level >= 24 + @FunctionalInterface + private interface VerifiableAction { + StatusResponse run(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/SetupActionResult.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/SetupActionResult.java new file mode 100644 index 0000000000..b4c881bfcb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/SetupActionResult.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; + +public class SetupActionResult { + private final ResultType resultType; + private String message; + private Exception exception; + private SetupProgress setupProgress; + + public SetupActionResult(ResultType resultType) { + this.resultType = resultType; + } + + public SetupActionResult message(String message) { + this.message = message; + return this; + } + + public SetupActionResult exception(Exception ex) { + exception = ex; + return this; + } + + public SetupActionResult setupProgress(SetupProgress setupProgress) { + this.setupProgress = setupProgress; + return this; + } + + public ResultType getResultType() { + return resultType; + } + + public String getMessage() { + return message; + } + + public Exception getException() { + return exception; + } + + public SetupProgress getSetupProgress() { + return setupProgress; + } + + public enum ResultType { + SUCCESS(true), + VERIFICATION_FAILURE(false), + FAILURE(false); + + private final boolean success; + + ResultType(boolean success) { + this.success = success; + } + + public boolean isSuccess() { + return success; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java new file mode 100644 index 0000000000..facdb4b2d5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.AcknowledgeAlertsCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class AcknowledgeAlertsAction implements OmnipodAction { + private final PodSessionState podState; + private final AlertSet alerts; + + public AcknowledgeAlertsAction(PodSessionState podState, AlertSet alerts) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (alerts == null) { + throw new ActionInitializationException("Alert set can not be null"); + } else if (alerts.size() == 0) { + throw new ActionInitializationException("Alert set can not be empty"); + } + this.podState = podState; + this.alerts = alerts; + } + + public AcknowledgeAlertsAction(PodSessionState podState, AlertSlot alertSlot) { + this(podState, new AlertSet(Collections.singletonList(alertSlot))); + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + return communicationService.sendCommand(StatusResponse.class, podState, + new AcknowledgeAlertsCommand(podState.getCurrentNonce(), alerts)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AssignAddressAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AssignAddressAction.java new file mode 100644 index 0000000000..b6563fa214 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AssignAddressAction.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.DateTimeZone; + +import java.util.Collections; +import java.util.Random; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.AssignAddressCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSetupState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodStateChangedHandler; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class AssignAddressAction implements OmnipodAction { + private final int address; + private final PodStateChangedHandler podStateChangedHandler; + + public AssignAddressAction(PodStateChangedHandler podStateChangedHandler) { + this.address = generateRandomAddress(); + this.podStateChangedHandler = podStateChangedHandler; + } + + private static int generateRandomAddress() { + return 0x1f000000 | (new Random().nextInt() & 0x000fffff); + } + + @Override + public PodSessionState execute(OmnipodCommunicationManager communicationService) { + PodSetupState setupState = new PodSetupState(address, 0x00, 0x00); + + AssignAddressCommand assignAddress = new AssignAddressCommand(setupState.getAddress()); + OmnipodMessage assignAddressMessage = new OmnipodMessage(OmnipodConst.DEFAULT_ADDRESS, + Collections.singletonList(assignAddress), setupState.getMessageNumber()); + + VersionResponse assignAddressResponse = communicationService.exchangeMessages(VersionResponse.class, setupState, assignAddressMessage, + OmnipodConst.DEFAULT_ADDRESS, setupState.getAddress()); + + DateTimeZone timeZone = DateTimeZone.getDefault(); + + PodSessionState podState = new PodSessionState(timeZone, address, assignAddressResponse.getPiVersion(), + assignAddressResponse.getPmVersion(), assignAddressResponse.getLot(), assignAddressResponse.getTid(), + setupState.getPacketNumber(), 0x00, communicationService.injector); // At this point, for an unknown reason, the pod starts counting messages from 0 again + + podState.setStateChangedHandler(podStateChangedHandler); + return podState; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java new file mode 100644 index 0000000000..14cee38962 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.BolusExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BolusDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class BolusAction implements OmnipodAction { + private final PodSessionState podState; + private final double units; + private final Duration timeBetweenPulses; + private final boolean acknowledgementBeep; + private final boolean completionBeep; + + public BolusAction(PodSessionState podState, double units, Duration timeBetweenPulses, + boolean acknowledgementBeep, boolean completionBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (timeBetweenPulses == null) { + throw new ActionInitializationException("Time between pulses cannot be null"); + } + this.podState = podState; + this.units = units; + this.timeBetweenPulses = timeBetweenPulses; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + } + + public BolusAction(PodSessionState podState, double units, boolean acknowledgementBeep, boolean completionBeep) { + this(podState, units, Duration.standardSeconds(2), acknowledgementBeep, completionBeep); + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + BolusDeliverySchedule bolusDeliverySchedule = new BolusDeliverySchedule(units, timeBetweenPulses); + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( + podState.getCurrentNonce(), bolusDeliverySchedule); + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(units, timeBetweenPulses, + acknowledgementBeep, completionBeep); + OmnipodMessage primeBolusMessage = new OmnipodMessage(podState.getAddress(), + Arrays.asList(setInsulinScheduleCommand, bolusExtraCommand), podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, primeBolusMessage); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java new file mode 100644 index 0000000000..ca713bcab6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.CancelDeliveryCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class CancelDeliveryAction implements OmnipodAction { + private final PodSessionState podState; + private final EnumSet deliveryTypes; + private final boolean acknowledgementBeep; + + public CancelDeliveryAction(PodSessionState podState, EnumSet deliveryTypes, + boolean acknowledgementBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (deliveryTypes == null) { + throw new ActionInitializationException("Delivery types cannot be null"); + } + this.podState = podState; + this.deliveryTypes = deliveryTypes; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + List messageBlocks = new ArrayList<>(); + + if (acknowledgementBeep && deliveryTypes.size() > 1) { + // Workaround for strange beep behaviour when cancelling multiple delivery types + List deliveryTypeList = new ArrayList<>(deliveryTypes); + + EnumSet deliveryTypeWithBeep = EnumSet.of(deliveryTypeList.remove(deliveryTypeList.size() - 1)); + EnumSet deliveryTypesWithoutBeep = EnumSet.copyOf(deliveryTypeList); + + messageBlocks.add(new CancelDeliveryCommand(podState.getCurrentNonce(), BeepType.NO_BEEP, deliveryTypesWithoutBeep)); + messageBlocks.add(new CancelDeliveryCommand(podState.getCurrentNonce(), BeepType.BEEP, deliveryTypeWithBeep)); + } else { + messageBlocks.add(new CancelDeliveryCommand(podState.getCurrentNonce(), + acknowledgementBeep && deliveryTypes.size() == 1 ? BeepType.BEEP : BeepType.NO_BEEP, deliveryTypes)); + } + + return communicationService.exchangeMessages(StatusResponse.class, podState, + new OmnipodMessage(podState.getAddress(), messageBlocks, podState.getMessageNumber())); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java new file mode 100644 index 0000000000..b44fc092a8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java @@ -0,0 +1,36 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.ConfigureAlertsCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class ConfigureAlertsAction implements OmnipodAction { + private final PodSessionState podState; + private final List alertConfigurations; + + public ConfigureAlertsAction(PodSessionState podState, List alertConfigurations) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (alertConfigurations == null) { + throw new ActionInitializationException("Alert configurations cannot be null"); + } + this.podState = podState; + this.alertConfigurations = alertConfigurations; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + ConfigureAlertsCommand configureAlertsCommand = new ConfigureAlertsCommand(podState.getCurrentNonce(), alertConfigurations); + StatusResponse statusResponse = communicationService.sendCommand(StatusResponse.class, podState, configureAlertsCommand); + for (AlertConfiguration alertConfiguration : alertConfigurations) { + podState.putConfiguredAlert(alertConfiguration.getAlertSlot(), alertConfiguration.getAlertType()); + } + return statusResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigurePodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigurePodAction.java new file mode 100644 index 0000000000..7d2cb3b7e3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigurePodAction.java @@ -0,0 +1,59 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.DateTime; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.ConfigurePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPodProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class ConfigurePodAction implements OmnipodAction { + private final PodSessionState podState; + + public ConfigurePodAction(PodSessionState podState) { + this.podState = podState; + } + + @Override + public VersionResponse execute(OmnipodCommunicationManager communicationService) { + if (!podState.getSetupProgress().equals(SetupProgress.ADDRESS_ASSIGNED)) { + throw new IllegalSetupProgressException(SetupProgress.ADDRESS_ASSIGNED, podState.getSetupProgress()); + } + DateTime activationDate = DateTime.now(podState.getTimeZone()); + + ConfigurePodCommand configurePodCommand = new ConfigurePodCommand(podState.getAddress(), activationDate, + podState.getLot(), podState.getTid()); + OmnipodMessage message = new OmnipodMessage(OmnipodConst.DEFAULT_ADDRESS, + Collections.singletonList(configurePodCommand), podState.getMessageNumber()); + VersionResponse configurePodResponse; + try { + configurePodResponse = communicationService.exchangeMessages(VersionResponse.class, podState, + message, OmnipodConst.DEFAULT_ADDRESS, podState.getAddress()); + } catch (IllegalPacketTypeException ex) { + if (PacketType.ACK.equals(ex.getActual())) { + // Pod is already configured + podState.setSetupProgress(SetupProgress.POD_CONFIGURED); + return null; + } + throw ex; + } + + if (configurePodResponse.getPodProgressStatus() != PodProgressStatus.PAIRING_SUCCESS) { + throw new IllegalPodProgressException(PodProgressStatus.PAIRING_SUCCESS, configurePodResponse.getPodProgressStatus()); + } + + podState.setSetupProgress(SetupProgress.POD_CONFIGURED); + + return configurePodResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java new file mode 100644 index 0000000000..01162ef434 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.DeactivatePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; + +public class DeactivatePodAction implements OmnipodAction { + private final PodSessionState podState; + private final boolean acknowledgementBeep; + + public DeactivatePodAction(PodSessionState podState, boolean acknowledgementBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + this.podState = podState; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + if (!podState.isSuspended() && !podState.hasFaultEvent()) { + try { + communicationService.executeAction(new CancelDeliveryAction(podState, + EnumSet.allOf(DeliveryType.class), acknowledgementBeep)); + } catch(PodFaultException ex) { + // Ignore + } + } + + return communicationService.sendCommand(StatusResponse.class, podState, new DeactivatePodCommand(podState.getCurrentNonce())); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java new file mode 100644 index 0000000000..0eef011607 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class GetPodInfoAction implements OmnipodAction { + private final PodSessionState podState; + private final PodInfoType podInfoType; + + public GetPodInfoAction(PodSessionState podState, PodInfoType podInfoType) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (podInfoType == null) { + throw new ActionInitializationException("Pod info type cannot be null"); + } + this.podState = podState; + this.podInfoType = podInfoType; + } + + @Override + public PodInfoResponse execute(OmnipodCommunicationManager communicationService) { + return communicationService.sendCommand(PodInfoResponse.class, podState, new GetStatusCommand(podInfoType)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java new file mode 100644 index 0000000000..dfcc1fe043 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class GetStatusAction implements OmnipodAction { + private final PodSessionState podState; + + public GetStatusAction(PodSessionState podState) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + this.podState = podState; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + return communicationService.sendCommand(StatusResponse.class, podState, new GetStatusCommand(PodInfoType.NORMAL)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java new file mode 100644 index 0000000000..9a433ab86a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java @@ -0,0 +1,71 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.InsertCannulaService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class InsertCannulaAction implements OmnipodAction { + + private final PodSessionState podState; + private final InsertCannulaService service; + private final BasalSchedule initialBasalSchedule; + + public InsertCannulaAction(InsertCannulaService insertCannulaService, PodSessionState podState, BasalSchedule initialBasalSchedule) { + if (insertCannulaService == null) { + throw new ActionInitializationException("Insert cannula service cannot be null"); + } + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (initialBasalSchedule == null) { + throw new ActionInitializationException("Initial basal schedule cannot be null"); + } + this.service = insertCannulaService; + this.podState = podState; + this.initialBasalSchedule = initialBasalSchedule; + } + + public static void updateCannulaInsertionStatus(PodSessionState podState, StatusResponse statusResponse, AAPSLogger aapsLogger) { + if (podState.getSetupProgress().equals(SetupProgress.CANNULA_INSERTING) && + statusResponse.getPodProgressStatus().isReadyForDelivery()) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Updating SetupProgress from CANNULA_INSERTING to COMPLETED"); + podState.setSetupProgress(SetupProgress.COMPLETED); + } + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + if (podState.getSetupProgress().isBefore(SetupProgress.PRIMING_FINISHED)) { + throw new IllegalSetupProgressException(SetupProgress.PRIMING_FINISHED, podState.getSetupProgress()); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.INITIAL_BASAL_SCHEDULE_SET)) { + service.programInitialBasalSchedule(communicationService, podState, initialBasalSchedule); + podState.setSetupProgress(SetupProgress.INITIAL_BASAL_SCHEDULE_SET); + } + if (podState.getSetupProgress().isBefore(SetupProgress.STARTING_INSERT_CANNULA)) { + service.executeExpirationRemindersAlertCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.STARTING_INSERT_CANNULA); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.CANNULA_INSERTING)) { + StatusResponse statusResponse = service.executeInsertionBolusCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.CANNULA_INSERTING); + return statusResponse; + } else if (podState.getSetupProgress().equals(SetupProgress.CANNULA_INSERTING)) { + // Check status + StatusResponse statusResponse = communicationService.executeAction(new GetStatusAction(podState)); + updateCannulaInsertionStatus(podState, statusResponse, communicationService.aapsLogger); + return statusResponse; + } else { + throw new IllegalSetupProgressException(null, podState.getSetupProgress()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java new file mode 100644 index 0000000000..f561a276d4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; + +public interface OmnipodAction { + T execute(OmnipodCommunicationManager communicationService); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java new file mode 100644 index 0000000000..1732ddf533 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PrimeService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class PrimeAction implements OmnipodAction { + + private final PrimeService service; + private final PodSessionState podState; + + public PrimeAction(PrimeService primeService, PodSessionState podState) { + if (primeService == null) { + throw new ActionInitializationException("Prime service cannot be null"); + } + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + this.service = primeService; + this.podState = podState; + } + + public static void updatePrimingStatus(PodSessionState podState, StatusResponse statusResponse, AAPSLogger aapsLogger) { + if (podState.getSetupProgress().equals(SetupProgress.PRIMING) && statusResponse.getPodProgressStatus().equals(PodProgressStatus.READY_FOR_BASAL_SCHEDULE)) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Updating SetupProgress from PRIMING to PRIMING_FINISHED"); + podState.setSetupProgress(SetupProgress.PRIMING_FINISHED); + } + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + if (podState.getSetupProgress().isBefore(SetupProgress.POD_CONFIGURED)) { + throw new IllegalSetupProgressException(SetupProgress.POD_CONFIGURED, podState.getSetupProgress()); + } + if (podState.getSetupProgress().isBefore(SetupProgress.STARTING_PRIME)) { + service.executeDisableTab5Sub16FaultConfigCommand(communicationService, podState); + service.executeFinishSetupReminderAlertCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.STARTING_PRIME); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.PRIMING)) { + StatusResponse statusResponse = service.executePrimeBolusCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.PRIMING); + return statusResponse; + } else if (podState.getSetupProgress().equals(SetupProgress.PRIMING)) { + // Check status + StatusResponse statusResponse = communicationService.executeAction(new GetStatusAction(podState)); + updatePrimingStatus(podState, statusResponse, communicationService.aapsLogger); + return statusResponse; + } else { + throw new IllegalSetupProgressException(null, podState.getSetupProgress()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java new file mode 100644 index 0000000000..0c8ae7f8be --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.BasalScheduleExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class SetBasalScheduleAction implements OmnipodAction { + private final PodSessionState podState; + private final BasalSchedule basalSchedule; + private final boolean confidenceReminder; + private final Duration scheduleOffset; + private final boolean acknowledgementBeep; + + public SetBasalScheduleAction(PodSessionState podState, BasalSchedule basalSchedule, + boolean confidenceReminder, Duration scheduleOffset, boolean acknowledgementBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (basalSchedule == null) { + throw new ActionInitializationException("Basal schedule cannot be null"); + } + if (scheduleOffset == null) { + throw new ActionInitializationException("Schedule offset cannot be null"); + } + this.podState = podState; + this.basalSchedule = basalSchedule; + this.confidenceReminder = confidenceReminder; + this.scheduleOffset = scheduleOffset; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + SetInsulinScheduleCommand setBasal = new SetInsulinScheduleCommand(podState.getCurrentNonce(), basalSchedule, scheduleOffset); + BasalScheduleExtraCommand extraCommand = new BasalScheduleExtraCommand(basalSchedule, scheduleOffset, + acknowledgementBeep, confidenceReminder, Duration.ZERO); + OmnipodMessage basalMessage = new OmnipodMessage(podState.getAddress(), Arrays.asList(setBasal, extraCommand), + podState.getMessageNumber()); + + StatusResponse statusResponse = communicationService.exchangeMessages(StatusResponse.class, podState, basalMessage); + podState.setBasalSchedule(basalSchedule); + return statusResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java new file mode 100644 index 0000000000..d9406a3ed7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.TempBasalExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; + +public class SetTempBasalAction implements OmnipodAction { + private final PodSessionState podState; + private final double rate; + private final Duration duration; + private final boolean acknowledgementBeep; + private final boolean completionBeep; + + public SetTempBasalAction(PodSessionState podState, double rate, Duration duration, + boolean acknowledgementBeep, boolean completionBeep) { + if (podState == null) { + throw new ActionInitializationException("Pod state cannot be null"); + } + if (duration == null) { + throw new ActionInitializationException("Duration cannot be null"); + } + this.podState = podState; + this.rate = rate; + this.duration = duration; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationManager communicationService) { + List messageBlocks = Arrays.asList( // + new SetInsulinScheduleCommand(podState.getCurrentNonce(), rate, duration), + new TempBasalExtraCommand(rate, duration, acknowledgementBeep, completionBeep, Duration.ZERO)); + + OmnipodMessage message = new OmnipodMessage(podState.getAddress(), messageBlocks, podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, message); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java new file mode 100644 index 0000000000..b897e448c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java @@ -0,0 +1,59 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigureAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetBasalScheduleAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfigurationFactory; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class InsertCannulaService { + public StatusResponse programInitialBasalSchedule(OmnipodCommunicationManager communicationService, + PodSessionState podState, BasalSchedule basalSchedule) { + return communicationService.executeAction(new SetBasalScheduleAction(podState, basalSchedule, + true, podState.getScheduleOffset(), false)); + } + + public StatusResponse executeExpirationRemindersAlertCommand(OmnipodCommunicationManager communicationService, + PodSessionState podState) { + AlertConfiguration lowReservoirAlertConfiguration = AlertConfigurationFactory.createLowReservoirAlertConfiguration(OmnipodConst.LOW_RESERVOIR_ALERT); + + DateTime endOfServiceTime = podState.getActivatedAt().plus(OmnipodConst.SERVICE_DURATION); + + Duration timeUntilExpirationAdvisoryAlarm = new Duration(DateTime.now(), + endOfServiceTime.minus(OmnipodConst.EXPIRATION_ADVISORY_WINDOW)); + Duration timeUntilShutdownImminentAlarm = new Duration(DateTime.now(), + endOfServiceTime.minus(OmnipodConst.END_OF_SERVICE_IMMINENT_WINDOW)); + + AlertConfiguration expirationAdvisoryAlertConfiguration = AlertConfigurationFactory.createExpirationAdvisoryAlertConfiguration( + timeUntilExpirationAdvisoryAlarm, OmnipodConst.EXPIRATION_ADVISORY_WINDOW); + AlertConfiguration shutdownImminentAlertConfiguration = AlertConfigurationFactory.createShutdownImminentAlertConfiguration( + timeUntilShutdownImminentAlarm); + AlertConfiguration autoOffAlertConfiguration = AlertConfigurationFactory.createAutoOffAlertConfiguration( + false, Duration.ZERO); + + List alertConfigurations = Arrays.asList( // + lowReservoirAlertConfiguration, // + expirationAdvisoryAlertConfiguration, // + shutdownImminentAlertConfiguration, // + autoOffAlertConfiguration // + ); + + return new ConfigureAlertsAction(podState, alertConfigurations).execute(communicationService); + } + + public StatusResponse executeInsertionBolusCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + return communicationService.executeAction(new BolusAction(podState, OmnipodConst.POD_CANNULA_INSERTION_BOLUS_UNITS, + Duration.standardSeconds(1), false, false)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java new file mode 100644 index 0000000000..3fc7663398 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.Duration; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigureAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.FaultConfigCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfigurationFactory; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PrimeService { + + public StatusResponse executeDisableTab5Sub16FaultConfigCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + FaultConfigCommand faultConfigCommand = new FaultConfigCommand(podState.getCurrentNonce(), (byte) 0x00, (byte) 0x00); + OmnipodMessage faultConfigMessage = new OmnipodMessage(podState.getAddress(), + Collections.singletonList(faultConfigCommand), podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, faultConfigMessage); + } + + public StatusResponse executeFinishSetupReminderAlertCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + AlertConfiguration finishSetupReminderAlertConfiguration = AlertConfigurationFactory.createFinishSetupReminderAlertConfiguration(); + return communicationService.executeAction(new ConfigureAlertsAction(podState, + Collections.singletonList(finishSetupReminderAlertConfiguration))); + } + + public StatusResponse executePrimeBolusCommand(OmnipodCommunicationManager communicationService, PodSessionState podState) { + return communicationService.executeAction(new BolusAction(podState, OmnipodConst.POD_PRIME_BOLUS_UNITS, + Duration.standardSeconds(1), false, false)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/ActionInitializationException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/ActionInitializationException.java new file mode 100644 index 0000000000..5682b0c6c7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/ActionInitializationException.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class ActionInitializationException extends OmnipodException { + public ActionInitializationException(String message) { + super(message, true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommandInitializationException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommandInitializationException.java new file mode 100644 index 0000000000..cb0553f292 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommandInitializationException.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class CommandInitializationException extends OmnipodException { + public CommandInitializationException(String message) { + super(message, true); + } + + public CommandInitializationException(String message, Throwable cause) { + super(message, cause, true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommunicationException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommunicationException.java new file mode 100644 index 0000000000..c7392fd42e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CommunicationException.java @@ -0,0 +1,36 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class CommunicationException extends OmnipodException { + private final Type type; + + public CommunicationException(Type type) { + super(type.getDescription(), false); + this.type = type; + } + + public CommunicationException(Type type, Throwable cause) { + super(type.getDescription() + ": "+ cause, cause, false); + this.type = type; + } + + public Type getType() { + return type; + } + + public enum Type { + TIMEOUT("Communication timeout"), + UNEXPECTED_EXCEPTION("Caught an unexpected Exception"); + + private final String description; + + Type(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CrcMismatchException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CrcMismatchException.java new file mode 100644 index 0000000000..3de98e6f49 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/CrcMismatchException.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class CrcMismatchException extends OmnipodException { + private final int expected; + private final int actual; + + public CrcMismatchException(int expected, int actual) { + super(String.format(Locale.getDefault(), "CRC mismatch: expected %d, got %d", expected, actual), false); + this.expected = expected; + this.actual = actual; + } + + public int getExpected() { + return expected; + } + + public int getActual() { + return actual; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalDeliveryStatusException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalDeliveryStatusException.java new file mode 100644 index 0000000000..01bf4db04f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalDeliveryStatusException.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalDeliveryStatusException extends OmnipodException { + private final DeliveryStatus expected; + private final DeliveryStatus actual; + + public IllegalDeliveryStatusException(DeliveryStatus expected, DeliveryStatus actual) { + super(String.format(Locale.getDefault(), "Illegal delivery status: %s, expected: %s", actual, expected), true); + this.expected = expected; + this.actual = actual; + } + + public DeliveryStatus getExpected() { + return expected; + } + + public DeliveryStatus getActual() { + return actual; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPacketTypeException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPacketTypeException.java new file mode 100644 index 0000000000..9856d3368c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPacketTypeException.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalPacketTypeException extends OmnipodException { + private final PacketType expected; + private final PacketType actual; + + public IllegalPacketTypeException(PacketType expected, PacketType actual) { + super(String.format(Locale.getDefault(), "Illegal packet type: %s, expected %s", + actual, expected), false); + this.expected = expected; + this.actual = actual; + } + + public PacketType getExpected() { + return expected; + } + + public PacketType getActual() { + return actual; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPodProgressException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPodProgressException.java new file mode 100644 index 0000000000..e4e575405e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalPodProgressException.java @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalPodProgressException extends OmnipodException { + private final PodProgressStatus expected; + private final PodProgressStatus actual; + + public IllegalPodProgressException(PodProgressStatus expected, PodProgressStatus actual) { + super(String.format(Locale.getDefault(), "Illegal setup state: %s, expected: %s", actual, expected), true); + this.expected = expected; + this.actual = actual; + } + + public PodProgressStatus getExpected() { + return expected; + } + + public PodProgressStatus getActual() { + return actual; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalResponseException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalResponseException.java new file mode 100644 index 0000000000..647309c478 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalResponseException.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalResponseException extends OmnipodException { + private final String actualClass; + private final MessageBlockType expectedType; + + public IllegalResponseException(String actualClass, MessageBlockType expectedType) { + super(String.format(Locale.getDefault(), "Illegal response type: got class of type %s " + + "for message block type %s", actualClass, expectedType), false); + this.actualClass = actualClass; + this.expectedType = expectedType; + } + + public String getActualClass() { + return actualClass; + } + + public MessageBlockType getExpectedType() { + return expectedType; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalSetupProgressException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalSetupProgressException.java new file mode 100644 index 0000000000..852d5ea3c2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/IllegalSetupProgressException.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class IllegalSetupProgressException extends OmnipodException { + private final SetupProgress expected; + private final SetupProgress actual; + + public IllegalSetupProgressException(SetupProgress expected, SetupProgress actual) { + super(String.format(Locale.getDefault(), "Illegal setup progress: %s, expected: %s", actual, expected), true); + this.expected = expected; + this.actual = actual; + } + + public SetupProgress getExpected() { + return expected; + } + + public SetupProgress getActual() { + return actual; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/MessageDecodingException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/MessageDecodingException.java new file mode 100644 index 0000000000..5e1c4f3523 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/MessageDecodingException.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class MessageDecodingException extends OmnipodException { + public MessageDecodingException(String message) { + super(message, false); + } + + public MessageDecodingException(String message, Throwable cause) { + super(message, cause, false); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceOutOfSyncException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceOutOfSyncException.java new file mode 100644 index 0000000000..929afbb855 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceOutOfSyncException.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class NonceOutOfSyncException extends OmnipodException { + public NonceOutOfSyncException() { + super("Nonce out of sync", true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceResyncException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceResyncException.java new file mode 100644 index 0000000000..a4910c9a42 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NonceResyncException.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class NonceResyncException extends OmnipodException { + public NonceResyncException() { + super("Nonce resync failed", true); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NotEnoughDataException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NotEnoughDataException.java new file mode 100644 index 0000000000..728fa4d734 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/NotEnoughDataException.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class NotEnoughDataException extends OmnipodException { + private final byte[] data; + + public NotEnoughDataException(byte[] data) { + super("Not enough data: " + ByteUtil.shortHexString(data), false); + this.data = data; + } + + public byte[] getData() { + return data; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodFaultException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodFaultException.java new file mode 100644 index 0000000000..6269f1c39c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodFaultException.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class PodFaultException extends OmnipodException { + private final PodInfoFaultEvent faultEvent; + + public PodFaultException(PodInfoFaultEvent faultEvent) { + super(faultEvent.getFaultEventType().toString(), true); + this.faultEvent = faultEvent; + } + + public PodInfoFaultEvent getFaultEvent() { + return faultEvent; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodReturnedErrorResponseException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodReturnedErrorResponseException.java new file mode 100644 index 0000000000..f43987b755 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/exception/PodReturnedErrorResponseException.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; + +public class PodReturnedErrorResponseException extends OmnipodException { + private final ErrorResponse errorResponse; + + public PodReturnedErrorResponseException(ErrorResponse errorResponse) { + super("Pod returned error response: " + errorResponse.getErrorResponseType(), true); + this.errorResponse = errorResponse; + } + + public ErrorResponse getErrorResponse() { + return errorResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java new file mode 100644 index 0000000000..2eec2afaf8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +public interface IRawRepresentable { + byte[] getRawData(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java new file mode 100644 index 0000000000..dba0035df7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public abstract class MessageBlock { + protected byte[] encodedData = new byte[0]; + + public MessageBlock() { + } + + public abstract MessageBlockType getType(); + + //This method returns raw message representation + //It should be rewritten in a derived class if raw representation of a concrete message + //is something else than just message type concatenated with message data + public byte[] getRawData() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + stream.write(this.getType().getValue()); + stream.write((byte) encodedData.length); + stream.write(encodedData); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return stream.toByteArray(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java new file mode 100644 index 0000000000..6170164366 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +public abstract class NonceResyncableMessageBlock extends MessageBlock { + public abstract int getNonce(); + + public abstract void setNonce(int nonce); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java new file mode 100644 index 0000000000..c328997172 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java @@ -0,0 +1,148 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.MessageDecodingException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; + +public class OmnipodMessage { + + private final int address; + private final List messageBlocks; + private final int sequenceNumber; + + public OmnipodMessage(OmnipodMessage other) { + address = other.address; + messageBlocks = new ArrayList<>(other.messageBlocks); + sequenceNumber = other.sequenceNumber; + } + + public OmnipodMessage(int address, List messageBlocks, int sequenceNumber) { + this.address = address; + this.messageBlocks = messageBlocks; + this.sequenceNumber = sequenceNumber; + } + + public static OmnipodMessage decodeMessage(byte[] data) { + if (data.length < 10) { + throw new NotEnoughDataException(data); + } + + int address = ByteUtil.toInt((int) data[0], (int) data[1], (int) data[2], + (int) data[3], ByteUtil.BitConversion.BIG_ENDIAN); + byte b9 = data[4]; + int bodyLength = ByteUtil.convertUnsignedByteToInt(data[5]); + if (data.length - 8 < bodyLength) { + throw new NotEnoughDataException(data); + } + int sequenceNumber = (((int) b9 >> 2) & 0b11111); + int crc = ByteUtil.toInt(data[data.length - 2], data[data.length - 1]); + int calculatedCrc = OmniCRC.crc16(ByteUtil.substring(data, 0, data.length - 2)); + if (crc != calculatedCrc) { + throw new CrcMismatchException(calculatedCrc, crc); + } + List blocks = decodeBlocks(ByteUtil.substring(data, 6, data.length - 6 - 2)); + if (blocks == null || blocks.size() == 0) { + throw new MessageDecodingException("No blocks decoded"); + } + + return new OmnipodMessage(address, blocks, sequenceNumber); + } + + private static List decodeBlocks(byte[] data) { + List blocks = new ArrayList<>(); + int index = 0; + while (index < data.length) { + try { + MessageBlockType blockType = MessageBlockType.fromByte(data[index]); + MessageBlock block = blockType.decode(ByteUtil.substring(data, index)); + blocks.add(block); + int blockLength = block.getRawData().length; + index += blockLength; + } catch (Exception ex) { + throw new MessageDecodingException("Failed to decode blocks", ex); + } + } + + return blocks; + } + + public byte[] getEncoded() { + byte[] encodedData = new byte[0]; + for (MessageBlock messageBlock : messageBlocks) { + encodedData = ByteUtil.concat(encodedData, messageBlock.getRawData()); + } + + byte[] header = new byte[0]; + //right before the message blocks we have 6 bits of seqNum and 10 bits of length + header = ByteUtil.concat(header, ByteUtil.getBytesFromInt(address)); + header = ByteUtil.concat(header, (byte) (((sequenceNumber & 0x1F) << 2) + ((encodedData.length >> 8) & 0x03))); + header = ByteUtil.concat(header, (byte) (encodedData.length & 0xFF)); + encodedData = ByteUtil.concat(header, encodedData); + int crc = OmniCRC.crc16(encodedData); + encodedData = ByteUtil.concat(encodedData, ByteUtil.substring(ByteUtil.getBytesFromInt(crc), 2, 2)); + return encodedData; + } + + public void padWithGetStatusCommands(int packetSize) { + while (getEncoded().length < packetSize) { + messageBlocks.add(new GetStatusCommand(PodInfoType.NORMAL)); + } + } + + public List getMessageBlocks() { + return messageBlocks; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + public boolean isNonceResyncable() { + return containsBlock(NonceResyncableMessageBlock.class); + } + + public int getSentNonce() { + for (MessageBlock messageBlock : messageBlocks) { + if (messageBlock instanceof NonceResyncableMessageBlock) { + return ((NonceResyncableMessageBlock) messageBlock).getNonce(); + } + } + throw new UnsupportedOperationException("Message is not nonce resyncable"); + } + + public void resyncNonce(int nonce) { + for (MessageBlock messageBlock : messageBlocks) { + if (messageBlock instanceof NonceResyncableMessageBlock) { + ((NonceResyncableMessageBlock) messageBlock).setNonce(nonce); + } + } + } + + public boolean containsBlock(Class blockType) { + for (MessageBlock messageBlock : messageBlocks) { + if (blockType.isInstance(messageBlock)) { + return true; + } + } + return false; + } + + + @Override + public String toString() { + return "OmnipodMessage{" + + "address=" + address + + ", messageBlocks=" + messageBlocks + + ", encoded=" + ByteUtil.shortHexStringWithoutSpaces(getEncoded()) + + ", sequenceNumber=" + sequenceNumber + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java new file mode 100644 index 0000000000..fd90ffb640 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java @@ -0,0 +1,82 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.RLMessage; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; + +/** + * Created by andy on 6/1/18. + */ +public class OmnipodPacket implements RLMessage { + private int packetAddress = 0; + private PacketType packetType = PacketType.INVALID; + private int sequenceNumber = 0; + private byte[] encodedMessage = null; + private boolean valid = false; + + public OmnipodPacket(byte[] encoded) { + if (encoded.length < 7) { + return; + } + this.packetAddress = ByteUtil.toInt((int) encoded[0], (int) encoded[1], + (int) encoded[2], (int) encoded[3], ByteUtil.BitConversion.BIG_ENDIAN); + try { + this.packetType = PacketType.fromByte((byte) (((int) encoded[4] & 0xFF) >> 5)); + } catch (IllegalArgumentException ex) { + throw new IllegalPacketTypeException(null, null); + } + this.sequenceNumber = (encoded[4] & 0b11111); + byte crc = OmniCRC.crc8(ByteUtil.substring(encoded, 0, encoded.length - 1)); + if (crc != encoded[encoded.length - 1]) { + throw new CrcMismatchException(crc, encoded[encoded.length - 1]); + } + this.encodedMessage = ByteUtil.substring(encoded, 5, encoded.length - 1 - 5); + valid = true; + } + + public OmnipodPacket(int packetAddress, PacketType packetType, int packetNumber, byte[] encodedMessage) { + this.packetAddress = packetAddress; + this.packetType = packetType; + this.sequenceNumber = packetNumber; + this.encodedMessage = encodedMessage; + if (encodedMessage.length > packetType.getMaxBodyLength()) { + this.encodedMessage = ByteUtil.substring(encodedMessage, 0, packetType.getMaxBodyLength()); + } + this.valid = true; + } + + public PacketType getPacketType() { + return packetType; + } + + public int getAddress() { + return packetAddress; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + public byte[] getEncodedMessage() { + return encodedMessage; + } + + @Override + public byte[] getTxData() { + byte[] output = new byte[0]; + output = ByteUtil.concat(output, ByteUtil.getBytesFromInt(this.packetAddress)); + output = ByteUtil.concat(output, (byte) ((this.packetType.getValue() << 5) + (sequenceNumber & 0b11111))); + output = ByteUtil.concat(output, encodedMessage); + output = ByteUtil.concat(output, OmniCRC.crc8(output)); + return output; + } + + @Override + public boolean isValid() { + return valid; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java new file mode 100644 index 0000000000..f429ed8b6f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java @@ -0,0 +1,54 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class AcknowledgeAlertsCommand extends NonceResyncableMessageBlock { + + private final AlertSet alerts; + private int nonce; + + public AcknowledgeAlertsCommand(int nonce, AlertSet alerts) { + this.nonce = nonce; + this.alerts = alerts; + encode(); + } + + public AcknowledgeAlertsCommand(int nonce, AlertSlot alertSlot) { + this(nonce, new AlertSet(Collections.singletonList(alertSlot))); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ACKNOWLEDGE_ALERT; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, alerts.getRawValue()); + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "AcknowledgeAlertsCommand{" + + "alerts=" + alerts + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java new file mode 100644 index 0000000000..e7a942885c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.nio.ByteBuffer; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class AssignAddressCommand extends MessageBlock { + private final int address; + + public AssignAddressCommand(int address) { + this.address = address; + encodedData = ByteBuffer.allocate(4).putInt(this.address).array(); + } + + public int getAddress() { + return address; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ASSIGN_ADDRESS; + } + + @Override + public String toString() { + return "AssignAddressCommand{" + + "address=" + address + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java new file mode 100644 index 0000000000..5d628537f9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java @@ -0,0 +1,127 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.RateEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalScheduleExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final byte currentEntryIndex; + private final double remainingPulses; + // We use a double for the delay between pulses because the Joda time API lacks precision for our calculations + private final double delayUntilNextTenthOfPulseInSeconds; + private final List rateEntries; + + public BasalScheduleExtraCommand(boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval, byte currentEntryIndex, + double remainingPulses, double delayUntilNextTenthOfPulseInSeconds, List rateEntries) { + + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + this.currentEntryIndex = currentEntryIndex; + this.remainingPulses = remainingPulses; + this.delayUntilNextTenthOfPulseInSeconds = delayUntilNextTenthOfPulseInSeconds; + this.rateEntries = rateEntries; + encode(); + } + + public BasalScheduleExtraCommand(BasalSchedule schedule, Duration scheduleOffset, + boolean acknowledgementBeep, boolean completionBeep, Duration programReminderInterval) { + rateEntries = new ArrayList<>(); + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + Duration scheduleOffsetNearestSecond = Duration.standardSeconds(Math.round(scheduleOffset.getMillis() / 1000.0)); + + BasalSchedule mergedSchedule = new BasalSchedule(schedule.adjacentEqualRatesMergedEntries()); + List durations = mergedSchedule.getDurations(); + + for (BasalSchedule.BasalScheduleDurationEntry entry : durations) { + rateEntries.addAll(RateEntry.createEntries(entry.getRate(), entry.getDuration())); + } + + BasalSchedule.BasalScheduleLookupResult entryLookupResult = mergedSchedule.lookup(scheduleOffsetNearestSecond); + currentEntryIndex = (byte) entryLookupResult.getIndex(); + double timeRemainingInEntryInSeconds = entryLookupResult.getStartTime().minus(scheduleOffsetNearestSecond.minus(entryLookupResult.getDuration())).getMillis() / 1000.0; + double rate = mergedSchedule.rateAt(scheduleOffsetNearestSecond); + int pulsesPerHour = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE); + double timeBetweenPulses = 3600.0 / pulsesPerHour; + delayUntilNextTenthOfPulseInSeconds = (timeRemainingInEntryInSeconds % (timeBetweenPulses / 10.0)); + remainingPulses = pulsesPerHour * (timeRemainingInEntryInSeconds - delayUntilNextTenthOfPulseInSeconds) / 3600.0 + 0.1; + + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + encodedData = new byte[]{ + beepOptions, + currentEntryIndex + }; + + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(remainingPulses * 10))); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) Math.round(delayUntilNextTenthOfPulseInSeconds * 1000 * 1000))); + + for (RateEntry entry : rateEntries) { + encodedData = ByteUtil.concat(encodedData, entry.getRawData()); + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BASAL_SCHEDULE_EXTRA; + } + + public boolean isAcknowledgementBeep() { + return acknowledgementBeep; + } + + public boolean isCompletionBeep() { + return completionBeep; + } + + public Duration getProgramReminderInterval() { + return programReminderInterval; + } + + public byte getCurrentEntryIndex() { + return currentEntryIndex; + } + + public double getRemainingPulses() { + return remainingPulses; + } + + public double getDelayUntilNextTenthOfPulseInSeconds() { + return delayUntilNextTenthOfPulseInSeconds; + } + + public List getRateEntries() { + return new ArrayList<>(rateEntries); + } + + @Override + public String toString() { + return "BasalScheduleExtraCommand{" + + "acknowledgementBeep=" + acknowledgementBeep + + ", completionBeep=" + completionBeep + + ", programReminderInterval=" + programReminderInterval + + ", currentEntryIndex=" + currentEntryIndex + + ", remainingPulses=" + remainingPulses + + ", delayUntilNextTenthOfPulseInSeconds=" + delayUntilNextTenthOfPulseInSeconds + + ", rateEntries=" + rateEntries + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java new file mode 100644 index 0000000000..9695548f85 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepConfigType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class BeepConfigCommand extends MessageBlock { + private final BeepConfigType beepType; + private final boolean basalCompletionBeep; + private final Duration basalIntervalBeep; + private final boolean tempBasalCompletionBeep; + private final Duration tempBasalIntervalBeep; + private final boolean bolusCompletionBeep; + private final Duration bolusIntervalBeep; + + public BeepConfigCommand(BeepConfigType beepType, boolean basalCompletionBeep, Duration basalIntervalBeep, + boolean tempBasalCompletionBeep, Duration tempBasalIntervalBeep, + boolean bolusCompletionBeep, Duration bolusIntervalBeep) { + this.beepType = beepType; + this.basalCompletionBeep = basalCompletionBeep; + this.basalIntervalBeep = basalIntervalBeep; + this.tempBasalCompletionBeep = tempBasalCompletionBeep; + this.tempBasalIntervalBeep = tempBasalIntervalBeep; + this.bolusCompletionBeep = bolusCompletionBeep; + this.bolusIntervalBeep = bolusIntervalBeep; + + encode(); + } + + public BeepConfigCommand(BeepConfigType beepType) { + this(beepType, false, Duration.ZERO, false, Duration.ZERO, false, Duration.ZERO); + } + + private void encode() { + encodedData = new byte[]{beepType.getValue()}; + encodedData = ByteUtil.concat(encodedData, (byte) ((basalCompletionBeep ? (1 << 6) : 0) + (basalIntervalBeep.getStandardMinutes() & 0x3f))); + encodedData = ByteUtil.concat(encodedData, (byte) ((tempBasalCompletionBeep ? (1 << 6) : 0) + (tempBasalIntervalBeep.getStandardMinutes() & 0x3f))); + encodedData = ByteUtil.concat(encodedData, (byte) ((bolusCompletionBeep ? (1 << 6) : 0) + (bolusIntervalBeep.getStandardMinutes() & 0x3f))); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BEEP_CONFIG; + } + + @Override + public String toString() { + return "BeepConfigCommand{" + + "beepType=" + beepType + + ", basalCompletionBeep=" + basalCompletionBeep + + ", basalIntervalBeep=" + basalIntervalBeep + + ", tempBasalCompletionBeep=" + tempBasalCompletionBeep + + ", tempBasalIntervalBeep=" + tempBasalIntervalBeep + + ", bolusCompletionBeep=" + bolusCompletionBeep + + ", bolusIntervalBeep=" + bolusIntervalBeep + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java new file mode 100644 index 0000000000..3554f85e0a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java @@ -0,0 +1,76 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BolusExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final double units; + private final Duration timeBetweenPulses; + private final double squareWaveUnits; + private final Duration squareWaveDuration; + + public BolusExtraCommand(double units, boolean acknowledgementBeep, boolean completionBeep) { + this(units, Duration.standardSeconds(2), acknowledgementBeep, completionBeep); + } + + public BolusExtraCommand(double units, Duration timeBetweenPulses, boolean acknowledgementBeep, boolean completionBeep) { + this(units, 0.0, Duration.ZERO, acknowledgementBeep, completionBeep, Duration.ZERO, timeBetweenPulses); + } + + public BolusExtraCommand(double units, double squareWaveUnits, Duration squareWaveDuration, + boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval, Duration timeBetweenPulses) { + if (units <= 0D) { + throw new CommandInitializationException("Units should be > 0"); + } else if (units > OmnipodConst.MAX_BOLUS) { + throw new CommandInitializationException("Units exceeds max bolus"); + } + this.units = units; + this.squareWaveUnits = squareWaveUnits; + this.squareWaveDuration = squareWaveDuration; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + this.timeBetweenPulses = timeBetweenPulses; + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + int squareWavePulseCountCountX10 = (int) Math.round(squareWaveUnits * 200); + int timeBetweenExtendedPulses = squareWavePulseCountCountX10 > 0 ? (int) squareWaveDuration.getMillis() * 100 / squareWavePulseCountCountX10 : 0; + + encodedData = ByteUtil.concat(encodedData, beepOptions); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(units * 200))); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) timeBetweenPulses.getMillis() * 100)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(squareWavePulseCountCountX10)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(timeBetweenExtendedPulses)); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BOLUS_EXTRA; + } + + @Override + public String toString() { + return "BolusExtraCommand{" + + "acknowledgementBeep=" + acknowledgementBeep + + ", completionBeep=" + completionBeep + + ", programReminderInterval=" + programReminderInterval + + ", units=" + units + + ", timeBetweenPulses=" + timeBetweenPulses + + ", squareWaveUnits=" + squareWaveUnits + + ", squareWaveDuration=" + squareWaveDuration + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java new file mode 100644 index 0000000000..451f9205f6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java @@ -0,0 +1,71 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class CancelDeliveryCommand extends NonceResyncableMessageBlock { + + private final BeepType beepType; + private final EnumSet deliveryTypes; + private int nonce; + + public CancelDeliveryCommand(int nonce, BeepType beepType, EnumSet deliveryTypes) { + this.nonce = nonce; + this.beepType = beepType; + this.deliveryTypes = deliveryTypes; + encode(); + } + + public CancelDeliveryCommand(int nonce, BeepType beepType, DeliveryType deliveryType) { + this(nonce, beepType, EnumSet.of(deliveryType)); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.CANCEL_DELIVERY; + } + + private void encode() { + encodedData = new byte[5]; + System.arraycopy(ByteUtil.getBytesFromInt(nonce), 0, encodedData, 0, 4); + byte beepTypeValue = beepType.getValue(); + if (beepTypeValue > 8) { + beepTypeValue = 0; + } + encodedData[4] = (byte) ((beepTypeValue & 0x0F) << 4); + if (deliveryTypes.contains(DeliveryType.BASAL)) { + encodedData[4] |= 1; + } + if (deliveryTypes.contains(DeliveryType.TEMP_BASAL)) { + encodedData[4] |= 2; + } + if (deliveryTypes.contains(DeliveryType.BOLUS)) { + encodedData[4] |= 4; + } + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "CancelDeliveryCommand{" + + "beepType=" + beepType + + ", deliveryTypes=" + deliveryTypes + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java new file mode 100644 index 0000000000..ac0a6a9f67 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ConfigureAlertsCommand extends NonceResyncableMessageBlock { + private final List configurations; + private int nonce; + + public ConfigureAlertsCommand(int nonce, List configurations) { + this.nonce = nonce; + this.configurations = configurations; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.CONFIGURE_ALERTS; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + for (AlertConfiguration config : configurations) { + encodedData = ByteUtil.concat(encodedData, config.getRawData()); + } + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "ConfigureAlertsCommand{" + + "configurations=" + configurations + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java new file mode 100644 index 0000000000..0f35a1fa24 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.DateTime; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ConfigurePodCommand extends MessageBlock { + + private static final byte PACKET_TIMEOUT_LIMIT = 0x04; + + private final int lot; + private final int tid; + private final DateTime date; + private final int address; + + public ConfigurePodCommand(int address, DateTime date, int lot, int tid) { + this.address = address; + this.lot = lot; + this.tid = tid; + this.date = date; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.SETUP_POD; + } + + private void encode() { + encodedData = new byte[0]; + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(address)); + encodedData = ByteUtil.concat(encodedData, new byte[]{ // + (byte) 0x14, // unknown + PACKET_TIMEOUT_LIMIT, // + (byte) date.monthOfYear().get(), // + (byte) date.dayOfMonth().get(), // + (byte) (date.year().get() - 2000), // + (byte) date.hourOfDay().get(), // + (byte) date.minuteOfHour().get() // + }); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(lot)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(tid)); + } + + @Override + public String toString() { + return "ConfigurePodCommand{" + + "lot=" + lot + + ", tid=" + tid + + ", date=" + date + + ", address=" + address + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java new file mode 100644 index 0000000000..554d778d14 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class DeactivatePodCommand extends NonceResyncableMessageBlock { + private int nonce; + + public DeactivatePodCommand(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.DEACTIVATE_POD; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "DeactivatePodCommand{" + + "nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java new file mode 100644 index 0000000000..9d165583e3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class FaultConfigCommand extends NonceResyncableMessageBlock { + private final byte tab5sub16; + private final byte tab5sub17; + private int nonce; + + public FaultConfigCommand(int nonce, byte tab5sub16, byte tab5sub17) { + this.nonce = nonce; + this.tab5sub16 = tab5sub16; + this.tab5sub17 = tab5sub17; + + encode(); + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, tab5sub16); + encodedData = ByteUtil.concat(encodedData, tab5sub17); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.FAULT_CONFIG; + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "FaultConfigCommand{" + + "tab5sub16=" + tab5sub16 + + ", tab5sub17=" + tab5sub17 + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java new file mode 100644 index 0000000000..68af71e277 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java @@ -0,0 +1,30 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class GetStatusCommand extends MessageBlock { + private final PodInfoType podInfoType; + + public GetStatusCommand(PodInfoType podInfoType) { + this.podInfoType = podInfoType; + encode(); + } + + private void encode() { + encodedData = new byte[]{podInfoType.getValue()}; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.GET_STATUS; + } + + @Override + public String toString() { + return "GetStatusCommand{" + + "podInfoType=" + podInfoType + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java new file mode 100644 index 0000000000..301435e6a2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java @@ -0,0 +1,98 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalDeliveryTable; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BolusDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.DeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.TempBasalDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class SetInsulinScheduleCommand extends NonceResyncableMessageBlock { + + private final DeliverySchedule schedule; + private int nonce; + + // Bolus + public SetInsulinScheduleCommand(int nonce, BolusDeliverySchedule schedule) { + this.nonce = nonce; + this.schedule = schedule; + encode(); + } + + // Basal schedule + public SetInsulinScheduleCommand(int nonce, BasalSchedule schedule, Duration scheduleOffset) { + int scheduleOffsetInSeconds = (int) scheduleOffset.getStandardSeconds(); + + BasalDeliveryTable table = new BasalDeliveryTable(schedule); + double rate = schedule.rateAt(scheduleOffset); + byte segment = (byte) (scheduleOffsetInSeconds / BasalDeliveryTable.SEGMENT_DURATION); + int segmentOffset = scheduleOffsetInSeconds % BasalDeliveryTable.SEGMENT_DURATION; + + int timeRemainingInSegment = BasalDeliveryTable.SEGMENT_DURATION - segmentOffset; + + double timeBetweenPulses = 3600 / (rate / OmnipodConst.POD_PULSE_SIZE); + + double offsetToNextTenth = timeRemainingInSegment % (timeBetweenPulses / 10.0); + + int pulsesRemainingInSegment = (int) ((timeRemainingInSegment + timeBetweenPulses / 10.0 - offsetToNextTenth) / timeBetweenPulses); + + this.nonce = nonce; + this.schedule = new BasalDeliverySchedule(segment, timeRemainingInSegment, pulsesRemainingInSegment, table); + encode(); + } + + // Temp basal + public SetInsulinScheduleCommand(int nonce, double tempBasalRate, Duration duration) { + if (tempBasalRate < 0D) { + throw new CommandInitializationException("Rate should be >= 0"); + } else if (tempBasalRate > OmnipodConst.MAX_BASAL_RATE) { + throw new CommandInitializationException("Rate exceeds max basal rate"); + } + if (duration.isLongerThan(OmnipodConst.MAX_TEMP_BASAL_DURATION)) { + throw new CommandInitializationException("Duration exceeds max temp basal duration"); + } + int pulsesPerHour = (int) Math.round(tempBasalRate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour / 2; + this.nonce = nonce; + this.schedule = new TempBasalDeliverySchedule(BasalDeliveryTable.SEGMENT_DURATION, pulsesPerSegment, new BasalDeliveryTable(tempBasalRate, duration)); + encode(); + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, schedule.getType().getValue()); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(schedule.getChecksum())); + encodedData = ByteUtil.concat(encodedData, schedule.getRawData()); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.SET_INSULIN_SCHEDULE; + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public String toString() { + return "SetInsulinScheduleCommand{" + + "schedule=" + schedule + + ", nonce=" + nonce + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java new file mode 100644 index 0000000000..212f748bf1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java @@ -0,0 +1,108 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.RateEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class TempBasalExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final double remainingPulses; + // We use a double for the delay until next pulse because the Joda time API lacks precision for our calculations + private final double delayUntilNextPulse; + private final List rateEntries; + + public TempBasalExtraCommand(double rate, Duration duration, boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval) { + if (rate < 0D) { + throw new CommandInitializationException("Rate should be >= 0"); + } else if (rate > OmnipodConst.MAX_BASAL_RATE) { + throw new CommandInitializationException("Rate exceeds max basal rate"); + } + if (duration.isLongerThan(OmnipodConst.MAX_TEMP_BASAL_DURATION)) { + throw new CommandInitializationException("Duration exceeds max temp basal duration"); + } + + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + + rateEntries = RateEntry.createEntries(rate, duration); + + RateEntry currentRateEntry = rateEntries.get(0); + remainingPulses = currentRateEntry.getTotalPulses(); + delayUntilNextPulse = currentRateEntry.getDelayBetweenPulsesInSeconds(); + + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + encodedData = new byte[]{ + beepOptions, + (byte) 0x00 + }; + + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(remainingPulses * 10))); + if (remainingPulses == 0) { + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) (delayUntilNextPulse * 1000 * 100) * 10)); + } else { + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) (delayUntilNextPulse * 1000 * 100))); + } + + for (RateEntry entry : rateEntries) { + encodedData = ByteUtil.concat(encodedData, entry.getRawData()); + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.TEMP_BASAL_EXTRA; + } + + public boolean isAcknowledgementBeep() { + return acknowledgementBeep; + } + + public boolean isCompletionBeep() { + return completionBeep; + } + + public Duration getProgramReminderInterval() { + return programReminderInterval; + } + + public double getRemainingPulses() { + return remainingPulses; + } + + public double getDelayUntilNextPulse() { + return delayUntilNextPulse; + } + + public List getRateEntries() { + return new ArrayList<>(rateEntries); + } + + @Override + public String toString() { + return "TempBasalExtraCommand{" + + "acknowledgementBeep=" + acknowledgementBeep + + ", completionBeep=" + completionBeep + + ", programReminderInterval=" + programReminderInterval + + ", remainingPulses=" + remainingPulses + + ", delayUntilNextPulse=" + delayUntilNextPulse + + ", rateEntries=" + rateEntries + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java new file mode 100644 index 0000000000..18f02475e1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.ErrorResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ErrorResponse extends MessageBlock { + private static final int MESSAGE_LENGTH = 5; + + private final ErrorResponseType errorResponseType; + private final int nonceSearchKey; + + public ErrorResponse(byte[] encodedData) { + if (encodedData.length < MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + this.encodedData = ByteUtil.substring(encodedData, 2, MESSAGE_LENGTH - 2); + + ErrorResponseType errorResponseType = null; + try { + errorResponseType = ErrorResponseType.fromByte(encodedData[2]); + } catch (IllegalArgumentException ex) { + } + + this.errorResponseType = errorResponseType; + this.nonceSearchKey = ByteUtil.makeUnsignedShort((int) encodedData[3], (int) encodedData[4]); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ERROR_RESPONSE; + } + + public ErrorResponseType getErrorResponseType() { + return errorResponseType; + } + + public int getNonceSearchKey() { + return nonceSearchKey; + } + + @Override + public String toString() { + return "ErrorResponse{" + + "errorResponseType=" + errorResponseType + + ", nonceSearchKey=" + nonceSearchKey + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java new file mode 100644 index 0000000000..a9d831f8ae --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java @@ -0,0 +1,119 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import org.joda.time.Duration; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class StatusResponse extends MessageBlock { + private static final int MESSAGE_LENGTH = 10; + + private final DeliveryStatus deliveryStatus; + private final PodProgressStatus podProgressStatus; + private final Duration timeActive; + private final Double reservoirLevel; + private final double insulinDelivered; + private final double insulinNotDelivered; + private final byte podMessageCounter; + private final AlertSet alerts; + + public StatusResponse(byte[] encodedData) { + if (encodedData.length < MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + this.encodedData = ByteUtil.substring(encodedData, 1, MESSAGE_LENGTH - 1); + + this.deliveryStatus = DeliveryStatus.fromByte((byte) (ByteUtil.convertUnsignedByteToInt(encodedData[1]) >>> 4)); + this.podProgressStatus = PodProgressStatus.fromByte((byte) (encodedData[1] & 0x0F)); + + int minutes = ((encodedData[7] & 0x7F) << 6) | ((encodedData[8] & 0xFC) >>> 2); + this.timeActive = Duration.standardMinutes(minutes); + + int highInsulinBits = (encodedData[2] & 0xF) << 9; + int middleInsulinBits = ByteUtil.convertUnsignedByteToInt(encodedData[3]) << 1; + int lowInsulinBits = ByteUtil.convertUnsignedByteToInt(encodedData[4]) >>> 7; + this.insulinDelivered = OmnipodConst.POD_PULSE_SIZE * (highInsulinBits | middleInsulinBits | lowInsulinBits); + this.podMessageCounter = (byte) ((encodedData[4] >>> 3) & 0xf); + + this.insulinNotDelivered = OmnipodConst.POD_PULSE_SIZE * (((encodedData[4] & 0x03) << 8) | ByteUtil.convertUnsignedByteToInt(encodedData[5])); + this.alerts = new AlertSet((byte) (((encodedData[6] & 0x7f) << 1) | (ByteUtil.convertUnsignedByteToInt(encodedData[7]) >>> 7))); + + double reservoirValue = (((encodedData[8] & 0x3) << 8) + ByteUtil.convertUnsignedByteToInt(encodedData[9])) * OmnipodConst.POD_PULSE_SIZE; + if (reservoirValue > OmnipodConst.MAX_RESERVOIR_READING) { + reservoirLevel = null; + } else { + reservoirLevel = reservoirValue; + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.STATUS_RESPONSE; + } + + public DeliveryStatus getDeliveryStatus() { + return deliveryStatus; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public Duration getTimeActive() { + return timeActive; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public double getInsulinDelivered() { + return insulinDelivered; + } + + public double getInsulinNotDelivered() { + return insulinNotDelivered; + } + + public byte getPodMessageCounter() { + return podMessageCounter; + } + + public AlertSet getAlerts() { + return alerts; + } + + public byte[] getRawData() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + stream.write(this.getType().getValue()); + stream.write(encodedData); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return stream.toByteArray(); + } + + @Override + public String toString() { + return "StatusResponse{" + + "deliveryStatus=" + deliveryStatus + + ", podProgressStatus=" + podProgressStatus + + ", timeActive=" + timeActive + + ", reservoirLevel=" + reservoirLevel + + ", insulinDelivered=" + insulinDelivered + + ", insulinNotDelivered=" + insulinNotDelivered + + ", podMessageCounter=" + podMessageCounter + + ", alerts=" + alerts + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java new file mode 100644 index 0000000000..a8c9e9403d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java @@ -0,0 +1,91 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FirmwareVersion; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; + +public class VersionResponse extends MessageBlock { + private final PodProgressStatus podProgressStatus; + private final FirmwareVersion pmVersion; + private final FirmwareVersion piVersion; + private final int lot; + private final int tid; + private final int address; + + public VersionResponse(byte[] encodedData) { + int length = ByteUtil.convertUnsignedByteToInt(encodedData[1]) + 2; + this.encodedData = ByteUtil.substring(encodedData, 2, length - 2); + + boolean extraByte; + byte[] truncatedData; + + switch (length) { + case 0x17: + truncatedData = ByteUtil.substring(encodedData, 2); + extraByte = true; + break; + case 0x1D: + truncatedData = ByteUtil.substring(encodedData, 9); + extraByte = false; + break; + default: + throw new IllegalArgumentException("Unrecognized VersionResponse message length: " + length); + } + + this.podProgressStatus = PodProgressStatus.fromByte(truncatedData[7]); + this.pmVersion = new FirmwareVersion(truncatedData[0], truncatedData[1], truncatedData[2]); + this.piVersion = new FirmwareVersion(truncatedData[3], truncatedData[4], truncatedData[5]); + this.lot = ByteUtil.toInt((int) truncatedData[8], (int) truncatedData[9], + (int) truncatedData[10], (int) truncatedData[11], ByteUtil.BitConversion.BIG_ENDIAN); + this.tid = ByteUtil.toInt((int) truncatedData[12], (int) truncatedData[13], + (int) truncatedData[14], (int) truncatedData[15], ByteUtil.BitConversion.BIG_ENDIAN); + + int indexIncrementor = extraByte ? 1 : 0; + + this.address = ByteUtil.toInt((int) truncatedData[16 + indexIncrementor], (int) truncatedData[17 + indexIncrementor], + (int) truncatedData[18 + indexIncrementor], (int) truncatedData[19 + indexIncrementor], ByteUtil.BitConversion.BIG_ENDIAN); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.VERSION_RESPONSE; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public FirmwareVersion getPmVersion() { + return pmVersion; + } + + public FirmwareVersion getPiVersion() { + return piVersion; + } + + public int getLot() { + return lot; + } + + public int getTid() { + return tid; + } + + public int getAddress() { + return address; + } + + @Override + public String toString() { + return "VersionResponse{" + + "podProgressStatus=" + podProgressStatus + + ", pmVersion=" + pmVersion + + ", piVersion=" + piVersion + + ", lot=" + lot + + ", tid=" + tid + + ", address=" + address + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java new file mode 100644 index 0000000000..9002f0d92e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public abstract class PodInfo { + private final byte[] encodedData; + + public PodInfo(byte[] encodedData) { + this.encodedData = encodedData; + } + + public abstract PodInfoType getType(); + + public byte[] getEncodedData() { + return encodedData; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java new file mode 100644 index 0000000000..1f32777fda --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java @@ -0,0 +1,92 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PodInfoActiveAlerts extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 11; + + private final byte[] word278; // Unknown use + private final List alertActivations; + + public PodInfoActiveAlerts(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + word278 = ByteUtil.substring(encodedData, 1, 2); + + alertActivations = new ArrayList<>(); + + for (AlertSlot alertSlot : AlertSlot.values()) { + int valueHighBits = ByteUtil.convertUnsignedByteToInt(encodedData[3 + alertSlot.getValue() * 2]); + int valueLowBits = ByteUtil.convertUnsignedByteToInt(encodedData[4 + alertSlot.getValue() * 2]); + int value = (valueHighBits << 8) | valueLowBits; + if (value != 0) { + alertActivations.add(new AlertActivation(alertSlot, value)); + } + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.ACTIVE_ALERTS; + } + + public byte[] getWord278() { + return word278; + } + + public List getAlertActivations() { + return new ArrayList<>(alertActivations); + } + + @Override + public String toString() { + return "PodInfoActiveAlerts{" + + "word278=" + Arrays.toString(word278) + + ", alertActivations=" + alertActivations + + '}'; + } + + public static class AlertActivation { + private final AlertSlot alertSlot; + private final int value; + + private AlertActivation(AlertSlot alertSlot, int value) { + this.alertSlot = alertSlot; + this.value = value; + } + + public double getValueAsUnits() { + return value * OmnipodConst.POD_PULSE_SIZE; + } + + public Duration getValueAsDuration() { + return Duration.standardMinutes(value); + } + + public AlertSlot getAlertSlot() { + return alertSlot; + } + + @Override + public String toString() { + return "AlertActivation{" + + "alertSlot=" + alertSlot + + ", valueAsUnits=" + getValueAsUnits() + + ", valueAsDuration=" + getValueAsDuration() + + '}'; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java new file mode 100644 index 0000000000..a8eebc7696 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java @@ -0,0 +1,83 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoDataLog extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 8; + private final FaultEventType faultEventType; + private final Duration timeFaultEvent; + private final Duration timeSinceActivation; + private final byte dataChunkSize; + private final byte maximumNumberOfDwords; + private final List dwords; + + public PodInfoDataLog(byte[] encodedData, int bodyLength) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + faultEventType = FaultEventType.fromByte(encodedData[1]); + timeFaultEvent = Duration.standardMinutes(ByteUtil.toInt(encodedData[2], encodedData[3])); + timeSinceActivation = Duration.standardMinutes(ByteUtil.toInt(encodedData[4], encodedData[5])); + dataChunkSize = encodedData[6]; + maximumNumberOfDwords = encodedData[7]; + + dwords = new ArrayList<>(); + + int numberOfDwords = (bodyLength - 8) / 4; + for (int i = 0; i < numberOfDwords; i++) { + dwords.add(ByteUtil.substring(encodedData, 8 + (4 * i), 4)); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.DATA_LOG; + } + + public FaultEventType getFaultEventType() { + return faultEventType; + } + + public Duration getTimeFaultEvent() { + return timeFaultEvent; + } + + public Duration getTimeSinceActivation() { + return timeSinceActivation; + } + + public byte getDataChunkSize() { + return dataChunkSize; + } + + public byte getMaximumNumberOfDwords() { + return maximumNumberOfDwords; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + @Override + public String toString() { + return "PodInfoDataLog{" + + "faultEventType=" + faultEventType + + ", timeFaultEvent=" + timeFaultEvent + + ", timeSinceActivation=" + timeSinceActivation + + ", dataChunkSize=" + dataChunkSize + + ", maximumNumberOfDwords=" + maximumNumberOfDwords + + ", dwords=" + dwords + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java new file mode 100644 index 0000000000..747b5e821b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java @@ -0,0 +1,54 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoFaultAndInitializationTime extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 17; + private final FaultEventType faultEventType; + private final Duration timeFaultEvent; + private final DateTime initializationTime; + + public PodInfoFaultAndInitializationTime(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + faultEventType = FaultEventType.fromByte(encodedData[1]); + timeFaultEvent = Duration.standardMinutes(((encodedData[2] & 0b1) << 8) + encodedData[3]); + // We ignore time zones here because we don't keep the time zone in which the pod was initially set up + // Which is fine because we don't use the initialization time for anything important anyway + initializationTime = new DateTime(2000 + encodedData[14], encodedData[12], encodedData[13], encodedData[15], encodedData[16]); + } + + @Override + public PodInfoType getType() { + return PodInfoType.FAULT_AND_INITIALIZATION_TIME; + } + + public FaultEventType getFaultEventType() { + return faultEventType; + } + + public Duration getTimeFaultEvent() { + return timeFaultEvent; + } + + public DateTime getInitializationTime() { + return initializationTime; + } + + @Override + public String toString() { + return "PodInfoFaultAndInitializationTime{" + + "faultEventType=" + faultEventType + + ", timeFaultEvent=" + timeFaultEvent + + ", initializationTime=" + initializationTime + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java new file mode 100644 index 0000000000..9db41a12f7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java @@ -0,0 +1,172 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.LogEventErrorCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PodInfoFaultEvent extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 21; + + private final PodProgressStatus podProgressStatus; + private final DeliveryStatus deliveryStatus; + private final double insulinNotDelivered; + private final byte podMessageCounter; + private final double totalInsulinDelivered; + private final FaultEventType faultEventType; + private final Duration faultEventTime; + private final Double reservoirLevel; + private final Duration timeSinceActivation; + private final AlertSet unacknowledgedAlerts; + private final boolean faultAccessingTables; + private final LogEventErrorCode logEventErrorType; + private final PodProgressStatus logEventErrorPodProgressStatus; + private final byte receiverLowGain; + private final byte radioRSSI; + private final PodProgressStatus podProgressStatusAtTimeOfFirstLoggedFaultEvent; + private final byte[] unknownValue; + + public PodInfoFaultEvent(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + podProgressStatus = PodProgressStatus.fromByte(encodedData[1]); + deliveryStatus = DeliveryStatus.fromByte(encodedData[2]); + insulinNotDelivered = OmnipodConst.POD_PULSE_SIZE * ByteUtil.toInt(encodedData[3], encodedData[4]); + podMessageCounter = encodedData[5]; + totalInsulinDelivered = OmnipodConst.POD_PULSE_SIZE * ByteUtil.toInt(encodedData[6], encodedData[7]); + faultEventType = FaultEventType.fromByte(encodedData[8]); + + int minutesSinceActivation = ByteUtil.toInt(encodedData[9], encodedData[10]); + if (minutesSinceActivation == 0xffff) { + faultEventTime = null; + } else { + faultEventTime = Duration.standardMinutes(minutesSinceActivation); + } + + double reservoirValue = ((encodedData[11] & 0x03) << 8) + + ByteUtil.convertUnsignedByteToInt(encodedData[12]) * OmnipodConst.POD_PULSE_SIZE; + if (reservoirValue > OmnipodConst.MAX_RESERVOIR_READING) { + reservoirLevel = null; + } else { + reservoirLevel = reservoirValue; + } + + int minutesActive = ByteUtil.toInt(encodedData[13], encodedData[14]); + timeSinceActivation = Duration.standardMinutes(minutesActive); + + unacknowledgedAlerts = new AlertSet(encodedData[15]); + faultAccessingTables = encodedData[16] == 0x02; + logEventErrorType = LogEventErrorCode.fromByte((byte) (encodedData[17] >>> 4)); + logEventErrorPodProgressStatus = PodProgressStatus.fromByte((byte) (encodedData[17] & 0x0f)); + receiverLowGain = (byte) (ByteUtil.convertUnsignedByteToInt(encodedData[18]) >>> 6); + radioRSSI = (byte) (encodedData[18] & 0x3f); + podProgressStatusAtTimeOfFirstLoggedFaultEvent = PodProgressStatus.fromByte((byte) (encodedData[19] & 0x0f)); + unknownValue = ByteUtil.substring(encodedData, 20, 2); + } + + @Override + public PodInfoType getType() { + return PodInfoType.FAULT_EVENT; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public DeliveryStatus getDeliveryStatus() { + return deliveryStatus; + } + + public double getInsulinNotDelivered() { + return insulinNotDelivered; + } + + public byte getPodMessageCounter() { + return podMessageCounter; + } + + public double getTotalInsulinDelivered() { + return totalInsulinDelivered; + } + + public FaultEventType getFaultEventType() { + return faultEventType; + } + + public Duration getFaultEventTime() { + return faultEventTime; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public Duration getTimeSinceActivation() { + return timeSinceActivation; + } + + public AlertSet getUnacknowledgedAlerts() { + return unacknowledgedAlerts; + } + + public boolean isFaultAccessingTables() { + return faultAccessingTables; + } + + public LogEventErrorCode getLogEventErrorType() { + return logEventErrorType; + } + + public PodProgressStatus getLogEventErrorPodProgressStatus() { + return logEventErrorPodProgressStatus; + } + + public byte getReceiverLowGain() { + return receiverLowGain; + } + + public byte getRadioRSSI() { + return radioRSSI; + } + + public PodProgressStatus getPodProgressStatusAtTimeOfFirstLoggedFaultEvent() { + return podProgressStatusAtTimeOfFirstLoggedFaultEvent; + } + + public byte[] getUnknownValue() { + return unknownValue; + } + + @Override + public String toString() { + return "PodInfoFaultEvent{" + + "podProgressStatus=" + podProgressStatus + + ", deliveryStatus=" + deliveryStatus + + ", insulinNotDelivered=" + insulinNotDelivered + + ", podMessageCounter=" + podMessageCounter + + ", totalInsulinDelivered=" + totalInsulinDelivered + + ", faultEventType=" + faultEventType + + ", faultEventTime=" + faultEventTime + + ", reservoirLevel=" + reservoirLevel + + ", timeSinceActivation=" + timeSinceActivation + + ", unacknowledgedAlerts=" + unacknowledgedAlerts + + ", faultAccessingTables=" + faultAccessingTables + + ", logEventErrorType=" + logEventErrorType + + ", logEventErrorPodProgressStatus=" + logEventErrorPodProgressStatus + + ", receiverLowGain=" + receiverLowGain + + ", radioRSSI=" + radioRSSI + + ", podProgressStatusAtTimeOfFirstLoggedFaultEvent=" + podProgressStatusAtTimeOfFirstLoggedFaultEvent + + ", unknownValue=" + ByteUtil.shortHexString(unknownValue) + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLog.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLog.java new file mode 100644 index 0000000000..c36b053cce --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderPulseLog.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoOlderPulseLog extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 3; + + private final ArrayList dwords; + + public PodInfoOlderPulseLog(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + dwords = new ArrayList<>(); + + int numberOfDwordLogEntries = ByteUtil.toInt(encodedData[1], encodedData[2]); + for (int i = 0; numberOfDwordLogEntries > i; i++) { + byte[] dword = ByteUtil.substring(encodedData, 3 + (4 * i), 4); + dwords.add(dword); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.OLDER_PULSE_LOG; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + @Override + public String toString() { + String out = "PodInfoOlderPulseLog{" + + "dwords=["; + + List hexDwords = new ArrayList<>(); + for (byte[] dword : dwords) { + hexDwords.add(ByteUtil.shortHexStringWithoutSpaces(dword)); + } + out += TextUtils.join(", ", hexDwords); + out += "]}"; + + return out; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLog.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLog.java new file mode 100644 index 0000000000..1008cca689 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentPulseLog.java @@ -0,0 +1,64 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoRecentPulseLog extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 3; + + private final ArrayList dwords; + + private final int lastEntryIndex; + + public PodInfoRecentPulseLog(byte[] encodedData, int bodyLength) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + lastEntryIndex = ByteUtil.toInt(encodedData[1], encodedData[2]); + dwords = new ArrayList<>(); + + int numberOfDwords = (bodyLength - 3) / 4; + + for (int i = 0; numberOfDwords > i; i++) { + byte[] dword = ByteUtil.substring(encodedData, 3 + (4 * i), 4); + dwords.add(dword); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.RECENT_PULSE_LOG; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + public int getLastEntryIndex() { + return lastEntryIndex; + } + + @Override + public String toString() { + String out = "PodInfoRecentPulseLog{" + + "lastEntryIndex=" + lastEntryIndex + + ",dwords=["; + + List hexDwords = new ArrayList<>(); + for (byte[] dword : dwords) { + hexDwords.add(ByteUtil.shortHexStringWithoutSpaces(dword)); + } + out += TextUtils.join(", ", hexDwords); + out += "]}"; + return out; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java new file mode 100644 index 0000000000..59322f6209 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoResponse extends MessageBlock { + private final PodInfoType subType; + private final PodInfo podInfo; + + public PodInfoResponse(byte[] encodedData) { + int bodyLength = ByteUtil.convertUnsignedByteToInt(encodedData[1]); + + this.encodedData = ByteUtil.substring(encodedData, 2, bodyLength); + subType = PodInfoType.fromByte(encodedData[2]); + podInfo = subType.decode(this.encodedData, bodyLength); + } + + public PodInfoType getSubType() { + return subType; + } + + public T getPodInfo() { + return (T) podInfo; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.POD_INFO_RESPONSE; + } + + @Override + public String toString() { + return "PodInfoResponse{" + + "subType=" + subType.name() + + ", podInfo=" + podInfo.toString() + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java new file mode 100644 index 0000000000..6c48954cfe --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java @@ -0,0 +1,71 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class AlertConfiguration { + private final AlertType alertType; + private final AlertSlot alertSlot; + private final boolean active; + private final boolean autoOffModifier; + private final Duration duration; + private final AlertTrigger alertTrigger; + private final BeepRepeat beepRepeat; + private final BeepType beepType; + + public AlertConfiguration(AlertType alertType, AlertSlot alertSlot, boolean active, boolean autoOffModifier, + Duration duration, AlertTrigger alertTrigger, + BeepType beepType, BeepRepeat beepRepeat) { + this.alertType = alertType; + this.alertSlot = alertSlot; + this.active = active; + this.autoOffModifier = autoOffModifier; + this.duration = duration; + this.alertTrigger = alertTrigger; + this.beepRepeat = beepRepeat; + this.beepType = beepType; + } + + public AlertType getAlertType() { + return alertType; + } + + public AlertSlot getAlertSlot() { + return alertSlot; + } + + public byte[] getRawData() { + int firstByte = (alertSlot.getValue() << 4); + firstByte += active ? (1 << 3) : 0; + + if (alertTrigger instanceof UnitsRemainingAlertTrigger) { + firstByte += 1 << 2; + } + + if (autoOffModifier) { + firstByte += 1 << 1; + } + + firstByte += ((int) duration.getStandardMinutes() >>> 8) & 0x1; + + byte[] encodedData = new byte[]{ + (byte) firstByte, + (byte) duration.getStandardMinutes() + }; + + if (alertTrigger instanceof UnitsRemainingAlertTrigger) { + int ticks = (int) (((UnitsRemainingAlertTrigger) alertTrigger).getValue() / OmnipodConst.POD_PULSE_SIZE / 2); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(ticks)); + } else if (alertTrigger instanceof TimerAlertTrigger) { + int durationInMinutes = (int) ((TimerAlertTrigger) alertTrigger).getValue().getStandardMinutes(); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(durationInMinutes)); + } + + encodedData = ByteUtil.concat(encodedData, beepRepeat.getValue()); + encodedData = ByteUtil.concat(encodedData, beepType.getValue()); + + return encodedData; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java new file mode 100644 index 0000000000..400fe18875 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +public class AlertConfigurationFactory { + public static AlertConfiguration createLowReservoirAlertConfiguration(Double units) { + return new AlertConfiguration(AlertType.LOW_RESERVOIR_ALERT, AlertSlot.SLOT4, true, false, Duration.ZERO, + new UnitsRemainingAlertTrigger(units), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_60_MINUTES); + } + + public static AlertConfiguration createExpirationAdvisoryAlertConfiguration(Duration timeUntilAlert, Duration duration) { + return new AlertConfiguration(AlertType.EXPIRATION_ADVISORY_ALERT, AlertSlot.SLOT7, true, false, duration, + new TimerAlertTrigger(timeUntilAlert), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_15_MINUTES); + } + + public static AlertConfiguration createShutdownImminentAlertConfiguration(Duration timeUntilAlert) { + return new AlertConfiguration(AlertType.SHUTDOWN_IMMINENT_ALARM, AlertSlot.SLOT2, true, false, Duration.ZERO, + new TimerAlertTrigger(timeUntilAlert), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_15_MINUTES); + } + + public static AlertConfiguration createAutoOffAlertConfiguration(boolean active, Duration countdownDuration) { + return new AlertConfiguration(AlertType.AUTO_OFF_ALARM, AlertSlot.SLOT0, active, true, + Duration.standardMinutes(15), new TimerAlertTrigger(countdownDuration), + BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_MINUTE_FOR_15_MINUTES); + } + + public static AlertConfiguration createFinishSetupReminderAlertConfiguration() { + return new AlertConfiguration(AlertType.FINISH_SETUP_REMINDER, AlertSlot.SLOT7, true, false, + Duration.standardMinutes(55), new TimerAlertTrigger(Duration.standardMinutes(5)), + BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_5_MINUTES); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java new file mode 100644 index 0000000000..9d65d7d5aa --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.ArrayList; +import java.util.List; + +public class AlertSet { + private final List alertSlots; + + public AlertSet(byte rawValue) { + alertSlots = new ArrayList<>(); + for (AlertSlot alertSlot : AlertSlot.values()) { + if ((alertSlot.getBitMaskValue() & rawValue) != 0) { + alertSlots.add(alertSlot); + } + } + } + + public AlertSet(List alertSlots) { + this.alertSlots = alertSlots; + } + + public List getAlertSlots() { + return new ArrayList<>(alertSlots); + } + + public int size() { + return alertSlots.size(); + } + + public byte getRawValue() { + byte value = 0; + for (AlertSlot alertSlot : alertSlots) { + value |= alertSlot.getBitMaskValue(); + } + return value; + } + + @Override + public String toString() { + return "AlertSet{" + + "alertSlots=" + alertSlots + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java new file mode 100644 index 0000000000..9f76ae0983 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java @@ -0,0 +1,35 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum AlertSlot { + SLOT0((byte) 0x00), + SLOT1((byte) 0x01), + SLOT2((byte) 0x02), + SLOT3((byte) 0x03), + SLOT4((byte) 0x04), + SLOT5((byte) 0x05), + SLOT6((byte) 0x06), + SLOT7((byte) 0x07); + + private byte value; + + AlertSlot(byte value) { + this.value = value; + } + + public static AlertSlot fromByte(byte value) { + for (AlertSlot type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown AlertSlot: " + value); + } + + public byte getBitMaskValue() { + return (byte) (1 << value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java new file mode 100644 index 0000000000..1dedb458d0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public abstract class AlertTrigger { + protected T value; + + public AlertTrigger(T value) { + this.value = value; + } + + public T getValue() { + return value; + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java new file mode 100644 index 0000000000..eb4b068a6f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum AlertType { + FINISH_PAIRING_REMINDER, + FINISH_SETUP_REMINDER, + EXPIRATION_ALERT, + EXPIRATION_ADVISORY_ALERT, + SHUTDOWN_IMMINENT_ALARM, + LOW_RESERVOIR_ALERT, + AUTO_OFF_ALARM +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepConfigType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepConfigType.java new file mode 100644 index 0000000000..3ef731e5c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepConfigType.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + + +// BeepConfigType is used only for the $1E Beep Config Command. +public enum BeepConfigType { + // 0x0 always returns an error response for Beep Config (use 0xF for no beep) + BEEP_BEEP_BEEP_BEEP((byte) 0x01), + BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP((byte) 0x02), + BIP_BIP((byte) 0x03), + BEEP((byte) 0x04), + BEEP_BEEP_BEEP((byte) 0x05), + BEEEEEEP((byte) 0x06), + BIP_BIP_BIP_BIP_BIP_BIP((byte) 0x07), + BEEEP_BEEEP((byte) 0x08), + // 0x9 and 0xA always return an error response for Beep Config + BEEP_BEEP((byte) 0xB), + BEEEP((byte) 0xC), + BIP_BEEEEEP((byte) 0xD), + FIVE_SECONDS_BEEP((byte) 0xE), // can only be used if Pod is currently suspended + NO_BEEP((byte) 0xF); + + private byte value; + + BeepConfigType(byte value) { + this.value = value; + } + + public static BeepConfigType fromByte(byte value) { + for (BeepConfigType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown BeepConfigType: " + value); + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java new file mode 100644 index 0000000000..3562dd6c18 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum BeepRepeat { + ONCE((byte) 0x00), + EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_60_MINUTES((byte) 0x01), + EVERY_MINUTE_FOR_15_MINUTES((byte) 0x02), + EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_15_MINUTES((byte) 0x03), + EVERY_3_MINUTES_DELAYED((byte) 0x04), + EVERY_60_MINUTES((byte) 0x05), + EVERY_15_MINUTES((byte) 0x06), + EVERY_15_MINUTES_DELAYED((byte) 0x07), + EVERY_5_MINUTES((byte) 0x08); + + private byte value; + + BeepRepeat(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java new file mode 100644 index 0000000000..e230276467 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +// BeepType is used for the $19 Configure Alerts and $1F Cancel Commands +public enum BeepType { + NO_BEEP((byte) 0x00), + BEEP_BEEP_BEEP_BEEP((byte) 0x01), + BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP((byte) 0x02), + BIP_BIP((byte) 0x03), + BEEP((byte) 0x04), + BEEP_BEEP_BEEP((byte) 0x05), + BEEEEEEP((byte) 0x06), + BIP_BIP_BIP_BIP_BIP_BIP((byte) 0x07), + BEEEP_BEEEP((byte) 0x08); + + private byte value; + + BeepType(byte value) { + this.value = value; + } + + public static BeepType fromByte(byte value) { + for (BeepType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown BeepType: " + value); + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java new file mode 100644 index 0000000000..9df1d480a5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java @@ -0,0 +1,33 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum DeliveryStatus { + SUSPENDED((byte) 0x00), + NORMAL((byte) 0x01), + TEMP_BASAL_RUNNING((byte) 0x02), + PRIMING((byte) 0x04), + BOLUS_IN_PROGRESS((byte) 0x05), + BOLUS_AND_TEMP_BASAL((byte) 0x06); + + private byte value; + + DeliveryStatus(byte value) { + this.value = value; + } + + public static DeliveryStatus fromByte(byte value) { + for (DeliveryStatus type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown DeliveryStatus: " + value); + } + + public byte getValue() { + return value; + } + + public boolean isBolusing() { + return this.equals(BOLUS_IN_PROGRESS) || this.equals(BOLUS_AND_TEMP_BASAL); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java new file mode 100644 index 0000000000..4d595afc15 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum DeliveryType { + NONE((byte) 0x00), + BASAL((byte) 0x01), + TEMP_BASAL((byte) 0x02), + BOLUS((byte) 0x04); + + private byte value; + + DeliveryType(byte value) { + this.value = value; + } + + public static DeliveryType fromByte(byte value) { + for (DeliveryType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown DeliveryType: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java new file mode 100644 index 0000000000..883ae94821 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum ErrorResponseType { + BAD_NONCE((byte) 0x14); + + private byte value; + + ErrorResponseType(byte value) { + this.value = value; + } + + public static ErrorResponseType fromByte(byte value) { + for (ErrorResponseType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown ErrorResponseType: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventType.java new file mode 100644 index 0000000000..69a74aa4c4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventType.java @@ -0,0 +1,148 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.Locale; + +public enum FaultEventType { + NO_FAULTS((byte) 0x00), + FAILED_FLASH_ERASE((byte) 0x01), + FAILED_FLASH_STORE((byte) 0x02), + TABLE_CORRUPTION_BASAL_SUBCOMMAND((byte) 0x03), + CORRUPTION_BYTE_720((byte) 0x05), + DATA_CORRUPTION_IN_TEST_RTC_INTERRUPT((byte) 0x06), + RTC_INTERRUPT_HANDLER_INCONSISTENT_STATE((byte) 0x07), + VALUE_GREATER_THAN_8((byte) 0x08), + BF_0_NOT_EQUAL_TO_BF_1((byte) 0x0A), + TABLE_CORRUPTION_TEMP_BASAL_SUBCOMMAND((byte) 0x0B), + RESET_DUE_TO_COP((byte) 0x0D), + RESET_DUE_TO_ILLEGAL_OPCODE((byte) 0x0E), + RESET_DUE_TO_ILLEGAL_ADDRESS((byte) 0x0F), + RESET_DUE_TO_SAWCOP((byte) 0x10), + CORRUPTION_IN_BYTE_866((byte) 0x11), + RESET_DUE_TO_LVD((byte) 0x12), + MESSAGE_LENGTH_TOO_LONG((byte) 0x13), + OCCLUDED((byte) 0x14), + CORRUPTION_IN_WORD_129((byte) 0x15), + CORRUPTION_IN_BYTE_868((byte) 0x16), + CORRUPTION_IN_A_VALIDATED_TABLE((byte) 0x17), + RESERVOIR_EMPTY((byte) 0x18), + BAD_POWER_SWITCH_ARRAY_VALUE_1((byte) 0x19), + BAD_POWER_SWITCH_ARRAY_VALUE_2((byte) 0x1A), + BAD_LOAD_CNTH_VALUE((byte) 0x1B), + EXCEEDED_MAXIMUM_POD_LIFE_80_HRS((byte) 0x1C), + BAD_STATE_COMMAND_1_A_SCHEDULE_PARSE((byte) 0x1D), + UNEXPECTED_STATE_IN_REGISTER_UPON_RESET((byte) 0x1E), + WRONG_SUMMARY_FOR_TABLE_129((byte) 0x1F), + VALIDATE_COUNT_ERROR_WHEN_BOLUSING((byte) 0x20), + BAD_TIMER_VARIABLE_STATE((byte) 0x21), + UNEXPECTED_RTC_MODULE_VALUE_DURING_RESET((byte) 0x22), + PROBLEM_CALIBRATE_TIMER((byte) 0x23), + RTC_INTERRUPT_HANDLER_UNEXPECTED_CALL((byte) 0x26), + MISSING_2_HOUR_ALERT_TO_FILL_TANK((byte) 0x27), + FAULT_EVENT_SETUP_POD((byte) 0x28), + ERROR_MAIN_LOOP_HELPER_0((byte) 0x29), + ERROR_MAIN_LOOP_HELPER_1((byte) 0x2A), + ERROR_MAIN_LOOP_HELPER_2((byte) 0x2B), + ERROR_MAIN_LOOP_HELPER_3((byte) 0x2C), + ERROR_MAIN_LOOP_HELPER_4((byte) 0x2D), + ERROR_MAIN_LOOP_HELPER_5((byte) 0x2E), + ERROR_MAIN_LOOP_HELPER_6((byte) 0x2F), + ERROR_MAIN_LOOP_HELPER_7((byte) 0x30), + INSULIN_DELIVERY_COMMAND_ERROR((byte) 0x31), + BAD_VALUE_STARTUP_TEST((byte) 0x32), + CONNECTED_POD_COMMAND_TIMEOUT((byte) 0x33), + RESET_FROM_UNKNOWN_CAUSE((byte) 0x34), + ERROR_FLASH_INITIALIZATION((byte) 0x36), + BAD_PIEZO_VALUE((byte) 0x37), + UNEXPECTED_VALUE_BYTE_358((byte) 0x38), + PROBLEM_WITH_LOAD_1_AND_2((byte) 0x39), + A_GREATER_THAN_7_IN_MESSAGE((byte) 0x3A), + FAILED_TEST_SAW_RESET((byte) 0x3B), + TEST_IN_PROGRESS((byte) 0x3C), + PROBLEM_WITH_PUMP_ANCHOR((byte) 0x3D), + ERROR_FLASH_WRITE((byte) 0x3E), + ENCODER_COUNT_TOO_HIGH((byte) 0x40), + ENCODER_COUNT_EXCESSIVE_VARIANCE((byte) 0x41), + ENCODER_COUNT_TOO_LOW((byte) 0x42), + ENCODER_COUNT_PROBLEM((byte) 0x43), + CHECK_VOLTAGE_OPEN_WIRE_1((byte) 0x44), + CHECK_VOLTAGE_OPEN_WIRE_2((byte) 0x45), + PROBLEM_WITH_LOAD_1_AND_2_TYPE_46((byte) 0x46), + PROBLEM_WITH_LOAD_1_AND_2_TYPE_47((byte) 0x47), + BAD_TIMER_CALIBRATION((byte) 0x48), + BAD_TIMER_RATIOS((byte) 0x49), + BAD_TIMER_VALUES((byte) 0x4A), + TRIM_ICS_TOO_CLOSE_TO_0_X_1_FF((byte) 0x4B), + PROBLEM_FINDING_BEST_TRIM_VALUE((byte) 0x4C), + BAD_SET_TPM_1_MULTI_CASES_VALUE((byte) 0x4D), + UNEXPECTED_RF_ERROR_FLAG_DURING_RESET((byte) 0x4F), + BAD_CHECK_SDRH_AND_BYTE_11_F_STATE((byte) 0x51), + ISSUE_TXO_KPROCESS_INPUT_BUFFER((byte) 0x52), + WRONG_VALUE_WORD_107((byte) 0x53), + PACKET_FRAME_LENGTH_TOO_LONG((byte) 0x54), + UNEXPECTED_IRQ_HIGHIN_TIMER_TICK((byte) 0x55), + UNEXPECTED_IRQ_LOWIN_TIMER_TICK((byte) 0x56), + BAD_ARG_TO_GET_ENTRY((byte) 0x57), + BAD_ARG_TO_UPDATE_37_A_TABLE((byte) 0x58), + ERROR_UPDATING_37_A_TABLE((byte) 0x59), + OCCLUSION_CHECK_VALUE_TOO_HIGH((byte) 0x5A), + LOAD_TABLE_CORRUPTION((byte) 0x5B), + PRIME_OPEN_COUNT_TOO_LOW((byte) 0x5C), + BAD_VALUE_BYTE_109((byte) 0x5D), + DISABLE_FLASH_SECURITY_FAILED((byte) 0x5E), + CHECK_VOLTAGE_FAILURE((byte) 0x5F), + OCCLUSION_CHECK_STARTUP_1((byte) 0x60), + OCCLUSION_CHECK_STARTUP_2((byte) 0x61), + OCCLUSION_CHECK_TIMEOUTS_1((byte) 0x62), + OCCLUSION_CHECK_TIMEOUTS_2((byte) 0x66), + OCCLUSION_CHECK_TIMEOUTS_3((byte) 0x67), + OCCLUSION_CHECK_PULSE_ISSUE((byte) 0x68), + OCCLUSION_CHECK_BOLUS_PROBLEM((byte) 0x69), + OCCLUSION_CHECK_ABOVE_THRESHOLD((byte) 0x6A), + BASAL_UNDER_INFUSION((byte) 0x80), + BASAL_OVER_INFUSION((byte) 0x81), + TEMP_BASAL_UNDER_INFUSION((byte) 0x82), + TEMP_BASAL_OVER_INFUSION((byte) 0x83), + BOLUS_UNDER_INFUSION((byte) 0x84), + BOLUS_OVER_INFUSION((byte) 0x85), + BASAL_OVER_INFUSION_PULSE((byte) 0x86), + TEMP_BASAL_OVER_INFUSION_PULSE((byte) 0x87), + BOLUS_OVER_INFUSION_PULSE((byte) 0x88), + IMMEDIATE_BOLUS_OVER_INFUSION_PULSE((byte) 0x89), + EXTENDED_BOLUS_OVER_INFUSION_PULSE((byte) 0x8A), + CORRUPTION_OF_TABLES((byte) 0x8B), + BAD_INPUT_TO_VERIFY_AND_START_PUMP((byte) 0x8D), + BAD_PUMP_REQ_5_STATE((byte) 0x8E), + COMMAND_1_A_PARSE_UNEXPECTED_FAILED((byte) 0x8F), + BAD_VALUE_FOR_TABLES((byte) 0x90), + BAD_PUMP_REQ_1_STATE((byte) 0x91), + BAD_PUMP_REQ_2_STATE((byte) 0x92), + BAD_PUMP_REQ_3_STATE((byte) 0x93), + BAD_VALUE_FIELD_6_IN_0_X_1_A((byte) 0x95), + BAD_STATE_IN_CLEAR_BOLUS_IST_2_AND_VARS((byte) 0x96), + BAD_STATE_IN_MAYBE_INC_33_D((byte) 0x97), + VALUES_DO_NOT_MATCH_OR_ARE_GREATER_THAN_0_X_97((byte) 0x98); + + private byte value; + + FaultEventType(byte value) { + this.value = value; + } + + public static FaultEventType fromByte(byte value) { + for (FaultEventType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown FaultEventType: " + value); + } + + public byte getValue() { + return value; + } + + @Override + public String toString() { + return String.format(Locale.getDefault(), "Pod fault (%d): %s", value, name()); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java new file mode 100644 index 0000000000..27dab2c091 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.Locale; + +public class FirmwareVersion { + private final int major; + private final int minor; + private final int patch; + + public FirmwareVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + @Override + public String toString() { + return String.format(Locale.getDefault(), "%d.%d.%d", major, minor, patch); + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getPatch() { + return patch; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java new file mode 100644 index 0000000000..1cf366fd61 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum LogEventErrorCode { + NONE((byte) 0x00), + IMMEDIATE_BOLUS_IN_PROGRESS((byte) 0x01), + INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_2((byte) 0x02), + INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_3((byte) 0x03), + INSULIN_STATE_TABLE_CORRUPTION((byte) 0x04); + + private byte value; + + LogEventErrorCode(byte value) { + this.value = value; + } + + public static LogEventErrorCode fromByte(byte value) { + for (LogEventErrorCode type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown LogEventErrorCode: " + value); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java new file mode 100644 index 0000000000..a7ced69305 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.apache.commons.lang3.NotImplementedException; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; + +public enum MessageBlockType { + VERSION_RESPONSE(0x01), + POD_INFO_RESPONSE(0x02), + SETUP_POD(0x03), + ERROR_RESPONSE(0x06), + ASSIGN_ADDRESS(0x07), + FAULT_CONFIG(0x08), + GET_STATUS(0x0e), + ACKNOWLEDGE_ALERT(0x11), + BASAL_SCHEDULE_EXTRA(0x13), + TEMP_BASAL_EXTRA(0x16), + BOLUS_EXTRA(0x17), + CONFIGURE_ALERTS(0x19), + SET_INSULIN_SCHEDULE(0x1a), + DEACTIVATE_POD(0x1c), + STATUS_RESPONSE(0x1d), + BEEP_CONFIG(0x1e), + CANCEL_DELIVERY(0x1f); + + private byte value; + + MessageBlockType(int value) { + this.value = (byte) value; + } + + public static MessageBlockType fromByte(byte value) { + for (MessageBlockType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown MessageBlockType: " + value); + } + + public byte getValue() { + return value; + } + + public MessageBlock decode(byte[] encodedData) { + switch (this) { + case VERSION_RESPONSE: + return new VersionResponse(encodedData); + case ERROR_RESPONSE: + return new ErrorResponse(encodedData); + case POD_INFO_RESPONSE: + return new PodInfoResponse(encodedData); + case STATUS_RESPONSE: + return new StatusResponse(encodedData); + default: + throw new NotImplementedException(this.name()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java new file mode 100644 index 0000000000..4698f88266 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.Arrays; + +public class NonceState { + private final long[] table = new long[21]; + private int index; + + public NonceState(int lot, int tid) { + initializeTable(lot, tid, (byte) 0x00); + } + + public NonceState(int lot, int tid, byte seed) { + initializeTable(lot, tid, seed); + } + + private void initializeTable(int lot, int tid, byte seed) { + table[0] = (long) (lot & 0xFFFF) + 0x55543DC3L + (((long) (lot) & 0xFFFFFFFFL) >> 16); + table[0] = table[0] & 0xFFFFFFFFL; + table[1] = (tid & 0xFFFF) + 0xAAAAE44EL + (((long) (tid) & 0xFFFFFFFFL) >> 16); + table[1] = table[1] & 0xFFFFFFFFL; + index = 0; + table[0] += seed; + for (int i = 0; i < 16; i++) { + table[2 + i] = generateEntry(); + } + index = (int) ((table[0] + table[1]) & 0X0F); + } + + private int generateEntry() { + table[0] = (((table[0] >> 16) + (table[0] & 0xFFFF) * 0x5D7FL) & 0xFFFFFFFFL); + table[1] = (((table[1] >> 16) + (table[1] & 0xFFFF) * 0x8CA0L) & 0xFFFFFFFFL); + return (int) ((table[1] + (table[0] << 16)) & 0xFFFFFFFFL); + } + + public int getCurrentNonce() { + return (int) table[(2 + index)]; + } + + public void advanceToNextNonce() { + int nonce = getCurrentNonce(); + table[(2 + index)] = generateEntry(); + index = (nonce & 0x0F); + } + + @Override + public String toString() { + return "NonceState{" + + "table=" + Arrays.toString(table) + + ", index=" + index + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommandType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommandType.java new file mode 100644 index 0000000000..bb0d40ef59 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommandType.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +/** + * Created by andy on 4.8.2019 + */ +public enum OmnipodCommandType { + + PairAndPrimePod, // + FillCanulaAndSetBasalProfile, // + //InitPod, // + DeactivatePod, // + SetBasalProfile, // + SetBolus, // + CancelBolus, // + SetTemporaryBasal, // + CancelTemporaryBasal, // + ResetPodStatus, // + GetPodStatus, // + SetTime, // + AcknowledgeAlerts, // + GetPodPulseLog; + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommunicationManagerInterface.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommunicationManagerInterface.java new file mode 100644 index 0000000000..3999cd2923 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCommunicationManagerInterface.java @@ -0,0 +1,79 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; + +public interface OmnipodCommunicationManagerInterface { + + // TODO add methods that can be used by OmniPod Eros and Omnipod Dash + + /** + * Initialize Pod + */ + PumpEnactResult initPod(PodInitActionType podInitActionType, PodInitReceiver podInitReceiver, Profile profile); + + /** + * Get Pod Status (is pod running, battery left ?, reservoir, etc) + */ + // TODO we should probably return a (wrapped) StatusResponse instead of a PumpEnactResult + PumpEnactResult getPodStatus(); + + /** + * Deactivate Pod + */ + PumpEnactResult deactivatePod(PodInitReceiver podInitReceiver); + + /** + * Set Basal Profile + */ + PumpEnactResult setBasalProfile(Profile basalProfile); + + /** + * Reset Pod status (if we forget to disconnect Pod and want to init new pod, and want to forget current pod) + */ + PumpEnactResult resetPodStatus(); + + /** + * Set Bolus + * + * @param detailedBolusInfo DetailedBolusInfo instance with amount and all other required data + */ + PumpEnactResult setBolus(DetailedBolusInfo detailedBolusInfo); + + /** + * Cancel Bolus (if bolus is already stopped, return acknowledgment) + */ + PumpEnactResult cancelBolus(); + + /** + * Set Temporary Basal + * + * @param tempBasalPair TempBasalPair object containg amount and duration in minutes + */ + PumpEnactResult setTemporaryBasal(TempBasalPair tempBasalPair); + + /** + * Cancel Temporary Basal (if TB is already stopped, return acknowledgment) + */ + PumpEnactResult cancelTemporaryBasal(); + + /** + * Acknowledge alerts + */ + PumpEnactResult acknowledgeAlerts(); + + /** + * Set Time on Pod + */ + PumpEnactResult setTime(); + + + void setPumpStatus(OmnipodPumpStatus pumpStatusLocal); + + + PodInfoRecentPulseLog readPulseLog(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCustomActionType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCustomActionType.java new file mode 100644 index 0000000000..edf7181f18 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodCustomActionType.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType; + +/** + * Created by andy on 4.8.2019 + */ + +public enum OmnipodCustomActionType implements CustomActionType { + + ResetRileyLinkConfiguration(), // + PairAndPrime(), // + FillCanulaSetBasalProfile(), // + //InitPod(), // + DeactivatePod(), // + ResetPodStatus(), // + ; + + @Override + public String getKey() { + return this.name(); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPodType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPodType.java new file mode 100644 index 0000000000..849a22e5b0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPodType.java @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum OmnipodPodType { + Eros, // + Dash +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPumpPluginInterface.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPumpPluginInterface.java new file mode 100644 index 0000000000..f2066ebb2b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodPumpPluginInterface.java @@ -0,0 +1,15 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.interfaces.PumpInterface; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; + +public interface OmnipodPumpPluginInterface extends PumpInterface { + + void addPodStatusRequest(OmnipodStatusRequest pumpStatusRequest); + + void setDriverState(OmnipodDriverState state); + + RxBusWrapper getRxBus(); + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodStatusRequest.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodStatusRequest.java new file mode 100644 index 0000000000..80f7e4ecfc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodStatusRequest.java @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum OmnipodStatusRequest { + ResetState(OmnipodCommandType.ResetPodStatus), // + AcknowledgeAlerts(OmnipodCommandType.AcknowledgeAlerts), // + GetPodState(OmnipodCommandType.GetPodStatus), // + GetPodPulseLog(OmnipodCommandType.GetPodPulseLog) + ; + + private OmnipodCommandType commandType; + + OmnipodStatusRequest(OmnipodCommandType commandType) { + this.commandType = commandType; + } + + + public OmnipodCommandType getCommandType() { + return commandType; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodUIResponseType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodUIResponseType.java new file mode 100644 index 0000000000..b135c3f590 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/OmnipodUIResponseType.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +/** + * Created by andy on 10/18/18. + */ + +public enum OmnipodUIResponseType { + + Data, + Error, + Invalid; + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java new file mode 100644 index 0000000000..aae862d9b8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PacketType { + INVALID((byte) 0), + POD((byte) 0b111), + PDM((byte) 0b101), + CON((byte) 0b100), + ACK((byte) 0b010); + + private byte value; + + PacketType(byte value) { + this.value = value; + } + + public static PacketType fromByte(byte value) { + for (PacketType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PacketType: " + value); + } + + public int getMaxBodyLength() { + switch (this) { + case ACK: + return 4; + case CON: + case PDM: + case POD: + return 31; + default: + return 0; + } + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodDeviceState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodDeviceState.java new file mode 100644 index 0000000000..5e903b5ae4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodDeviceState.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.R; + +/** + * Created by andy on 4.8.2019 + */ + +public enum PodDeviceState { + + // FIXME + NeverContacted(R.string.medtronic_pump_status_never_contacted), // + Sleeping(R.string.medtronic_pump_status_sleeping), // + WakingUp(R.string.medtronic_pump_status_waking_up), // + Active(R.string.medtronic_pump_status_active), // + ErrorWhenCommunicating(R.string.medtronic_pump_status_error_comm), // + TimeoutWhenCommunicating(R.string.medtronic_pump_status_timeout_comm), // + // ProblemContacting(R.string.medtronic_pump_status_problem_contacting), // + PumpUnreachable(R.string.medtronic_pump_status_pump_unreachable), // + InvalidConfiguration(R.string.medtronic_pump_status_invalid_config); + + Integer resourceId = null; + + + PodDeviceState() { + + } + + + PodDeviceState(int resourceId) { + this.resourceId = resourceId; + } + + + public Integer getResourceId() { + return resourceId; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java new file mode 100644 index 0000000000..4e2095e711 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfo; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoActiveAlerts; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoDataLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultAndInitializationTime; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoOlderPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; + +public enum PodInfoType { + NORMAL((byte) 0x00), + ACTIVE_ALERTS((byte) 0x01), + FAULT_EVENT((byte) 0x02), + DATA_LOG((byte) 0x03), // Similar to types $50 & $51. Returns up to the last 60 dwords of data. + FAULT_AND_INITIALIZATION_TIME((byte) 0x05), + RECENT_PULSE_LOG((byte) 0x50), // Starting at $4200 + OLDER_PULSE_LOG((byte) 0x51); // Starting at $4200 but dumps entries before the last 50 + + private final byte value; + + PodInfoType(byte value) { + this.value = value; + } + + public static PodInfoType fromByte(byte value) { + for (PodInfoType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PodInfoType: " + value); + } + + public byte getValue() { + return value; + } + + public PodInfo decode(byte[] encodedData, int bodyLength) { + switch (this) { + case NORMAL: + // We've never observed a PodInfoResponse with 0x00 subtype + // Instead, the pod returns a StatusResponse + throw new UnsupportedOperationException("Cannot decode PodInfoType.NORMAL"); + case ACTIVE_ALERTS: + return new PodInfoActiveAlerts(encodedData); + case FAULT_EVENT: + return new PodInfoFaultEvent(encodedData); + case DATA_LOG: + return new PodInfoDataLog(encodedData, bodyLength); + case FAULT_AND_INITIALIZATION_TIME: + return new PodInfoFaultAndInitializationTime(encodedData); + case RECENT_PULSE_LOG: + return new PodInfoRecentPulseLog(encodedData, bodyLength); + case OLDER_PULSE_LOG: + return new PodInfoOlderPulseLog(encodedData); + default: + throw new IllegalArgumentException("Cannot decode " + this.name()); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitActionType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitActionType.java new file mode 100644 index 0000000000..c0a92ed2d7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitActionType.java @@ -0,0 +1,88 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import info.nightscout.androidaps.R; + +public enum PodInitActionType { + + PairAndPrimeWizardStep(), // + PairPod(R.string.omnipod_init_pod_pair_pod, PairAndPrimeWizardStep), // + PrimePod(R.string.omnipod_init_pod_prime_pod, PairAndPrimeWizardStep), // + + FillCannulaSetBasalProfileWizardStep(), // + FillCannula(R.string.omnipod_init_pod_fill_cannula, FillCannulaSetBasalProfileWizardStep), // + SetBasalProfile(R.string.omnipod_init_pod_set_basal_profile, FillCannulaSetBasalProfileWizardStep), // + + DeactivatePodWizardStep(), // + CancelDelivery(R.string.omnipod_deactivate_pod_cancel_delivery, DeactivatePodWizardStep), // + DeactivatePod(R.string.omnipod_deactivate_pod_deactivate_pod, DeactivatePodWizardStep) // + ; + + + + private int resourceId; + private PodInitActionType parent; + + private static Map> stepsForWizardStep; + + + PodInitActionType(int resourceId, PodInitActionType parent) { + this.resourceId = resourceId; + this.parent = parent; + } + + + PodInitActionType() { + } + + + public boolean isParent() { + return this.parent == null; + } + + + public List getChildren() { + + List outList = new ArrayList<>(); + + for (PodInitActionType value : values()) { + if (value.parent == this) { + outList.add(value); + } + } + + return outList; + } + + + public static List getAvailableWizardSteps(OmnipodPodType podType) { + List outList = new ArrayList<>(); + + if (podType == OmnipodPodType.Eros) { + outList.add(PodInitActionType.PairAndPrimeWizardStep); + outList.add(PodInitActionType.FillCannulaSetBasalProfileWizardStep); + } else { + // TODO we might have different wizard steps, with different handling for Dash + } + + return outList; + } + + + public static List getAvailableActionsForWizardSteps(PodInitActionType wizardStep) { + if (stepsForWizardStep.containsKey(wizardStep)) { + return stepsForWizardStep.get(wizardStep); + } else { + return new ArrayList<>(); + } + } + + + public int getResourceId() { + return resourceId; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitReceiver.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitReceiver.java new file mode 100644 index 0000000000..789da1add2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInitReceiver.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public interface PodInitReceiver { + + void returnInitTaskStatus(PodInitActionType podInitActionType, boolean isSuccess, String errorMessage); + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java new file mode 100644 index 0000000000..4bd9780118 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PodProgressStatus { + INITIAL_VALUE((byte) 0x00), + TANK_POWER_ACTIVATED((byte) 0x01), + TANK_FILL_COMPLETED((byte) 0x02), + PAIRING_SUCCESS((byte) 0x03), + PRIMING((byte) 0x04), + READY_FOR_BASAL_SCHEDULE((byte) 0x05), + READY_FOR_CANNULA_INSERTION((byte) 0x06), + CANNULA_INSERTING((byte) 0x07), + RUNNING_ABOVE_FIFTY_UNITS((byte) 0x08), + RUNNING_BELOW_FIFTY_UNITS((byte) 0x09), + ONE_NOT_USED_BUT_IN_33((byte) 0x0a), + TWO_NOT_USED_BUT_IN_33((byte) 0x0b), + THREE_NOT_USED_BUT_IN_33((byte) 0x0c), + FAULT_EVENT_OCCURRED((byte) 0x0d), + FAILED_TO_INITIALIZE_IN_TIME((byte) 0x0e), + INACTIVE((byte) 0x0f); + + private byte value; + + PodProgressStatus(byte value) { + this.value = value; + } + + public static PodProgressStatus fromByte(byte value) { + for (PodProgressStatus type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PodProgressStatus: " + value); + } + + public byte getValue() { + return value; + } + + public boolean isReadyForDelivery() { + return this == RUNNING_ABOVE_FIFTY_UNITS || this == RUNNING_BELOW_FIFTY_UNITS; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodResponseType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodResponseType.java new file mode 100644 index 0000000000..7314ae9591 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodResponseType.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PodResponseType { + + Acknowledgment, // set commands would just acknowledge if data was sent + Data, // query commands would return data + Error, // communication/response produced an error + Invalid // invalid response (not supported, should never be returned) +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java new file mode 100644 index 0000000000..72a3b5438a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java @@ -0,0 +1,21 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum SetupProgress { + ADDRESS_ASSIGNED, + POD_CONFIGURED, + STARTING_PRIME, + PRIMING, + PRIMING_FINISHED, + INITIAL_BASAL_SCHEDULE_SET, + STARTING_INSERT_CANNULA, + CANNULA_INSERTING, + COMPLETED; + + public boolean isBefore(SetupProgress other) { + return this.ordinal() < other.ordinal(); + } + + public boolean isAfter(SetupProgress other) { + return this.ordinal() > other.ordinal(); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java new file mode 100644 index 0000000000..8d4096de59 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +public class TimerAlertTrigger extends AlertTrigger { + public TimerAlertTrigger(Duration value) { + super(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java new file mode 100644 index 0000000000..6668be13c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public class UnitsRemainingAlertTrigger extends AlertTrigger { + public UnitsRemainingAlertTrigger(Double value) { + super(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java new file mode 100644 index 0000000000..ab46dacd76 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class BasalDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final byte currentSegment; + private final int secondsRemaining; + private final int pulsesRemaining; + private final BasalDeliveryTable basalTable; + + public BasalDeliverySchedule(byte currentSegment, int secondsRemaining, int pulsesRemaining, + BasalDeliveryTable basalTable) { + this.currentSegment = currentSegment; + this.secondsRemaining = secondsRemaining; + this.pulsesRemaining = pulsesRemaining; + this.basalTable = basalTable; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, currentSegment); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(secondsRemaining << 3)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulsesRemaining)); + for (BasalTableEntry entry : basalTable.getEntries()) { + rawData = ByteUtil.concat(rawData, entry.getRawData()); + } + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.BASAL_SCHEDULE; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 5; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + for (BasalTableEntry entry : basalTable.getEntries()) { + checksum += entry.getChecksum(); + } + + return checksum; + } + + @Override + public String toString() { + return "BasalDeliverySchedule{" + + "currentSegment=" + currentSegment + + ", secondsRemaining=" + secondsRemaining + + ", pulsesRemaining=" + pulsesRemaining + + ", basalTable=" + basalTable + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java new file mode 100644 index 0000000000..3e18d6b5d0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java @@ -0,0 +1,112 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalDeliveryTable { + + public static final int SEGMENT_DURATION = 30 * 60; + public static final int MAX_PULSES_PER_RATE_ENTRY = 6400; + + private static final int NUM_SEGMENTS = 48; + private static final int MAX_SEGMENTS_PER_ENTRY = 16; + + private List entries = new ArrayList<>(); + + public BasalDeliveryTable(BasalSchedule schedule) { + TempSegment[] expandedSegments = new TempSegment[48]; + + boolean halfPulseRemainder = false; + for (int i = 0; i < NUM_SEGMENTS; i++) { + double rate = schedule.rateAt(Duration.standardMinutes(i * 30)); + int pulsesPerHour = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour >>> 1; + boolean halfPulse = (pulsesPerHour & 0b1) != 0; + + expandedSegments[i] = new TempSegment(pulsesPerSegment + (halfPulseRemainder && halfPulse ? 1 : 0)); + halfPulseRemainder = halfPulseRemainder != halfPulse; + } + + List segmentsToMerge = new ArrayList<>(); + + boolean altSegmentPulse = false; + for (TempSegment segment : expandedSegments) { + if (segmentsToMerge.isEmpty()) { + segmentsToMerge.add(segment); + continue; + } + + TempSegment firstSegment = segmentsToMerge.get(0); + + int delta = segment.getPulses() - firstSegment.getPulses(); + if (segmentsToMerge.size() == 1) { + altSegmentPulse = delta == 1; + } + + int expectedDelta = altSegmentPulse ? segmentsToMerge.size() % 2 : 0; + + if (expectedDelta != delta || segmentsToMerge.size() == MAX_SEGMENTS_PER_ENTRY) { + addBasalTableEntry(segmentsToMerge, altSegmentPulse); + segmentsToMerge.clear(); + } + + segmentsToMerge.add(segment); + } + + addBasalTableEntry(segmentsToMerge, altSegmentPulse); + } + + public BasalDeliveryTable(double tempBasalRate, Duration duration) { + int pulsesPerHour = (int) Math.round(tempBasalRate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour >> 1; + boolean alternateSegmentPulse = (pulsesPerHour & 0b1) != 0; + + int remaining = (int) Math.round(duration.getStandardSeconds() / (double) BasalDeliveryTable.SEGMENT_DURATION); + + while (remaining > 0) { + int segments = Math.min(MAX_SEGMENTS_PER_ENTRY, remaining); + entries.add(new BasalTableEntry(segments, pulsesPerSegment, segments > 1 && alternateSegmentPulse)); + remaining -= segments; + } + } + + private void addBasalTableEntry(List segments, boolean alternateSegmentPulse) { + entries.add(new BasalTableEntry(segments.size(), segments.get(0).getPulses(), alternateSegmentPulse)); + } + + public BasalTableEntry[] getEntries() { + return entries.toArray(new BasalTableEntry[0]); + } + + byte numSegments() { + byte numSegments = 0; + for (BasalTableEntry entry : entries) { + numSegments += entry.getSegments(); + } + return numSegments; + } + + @Override + public String toString() { + return "BasalDeliveryTable{" + + "entries=" + entries + + '}'; + } + + private class TempSegment { + private int pulses; + + public TempSegment(int pulses) { + this.pulses = pulses; + } + + public int getPulses() { + return pulses; + } + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java new file mode 100644 index 0000000000..140c584c0b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java @@ -0,0 +1,147 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class BasalSchedule { + private final List entries; + + public BasalSchedule(List entries) { + if (entries == null || entries.size() == 0) { + throw new IllegalArgumentException("Entries can not be empty"); + } else if (!entries.get(0).getStartTime().isEqual(Duration.ZERO)) { + throw new IllegalArgumentException("First basal schedule entry should have 0 offset"); + } + this.entries = entries; + } + + public double rateAt(Duration offset) { + return lookup(offset).getBasalScheduleEntry().getRate(); + } + + public List getEntries() { + return new ArrayList<>(entries); + } + + public BasalScheduleLookupResult lookup(Duration offset) { + if (offset.isLongerThan(Duration.standardHours(24)) || offset.isShorterThan(Duration.ZERO)) { + throw new IllegalArgumentException("Invalid duration"); + } + + List reversedBasalScheduleEntries = reversedBasalScheduleEntries(); + + Duration last = Duration.standardHours(24); + int index = 0; + for (BasalScheduleEntry entry : reversedBasalScheduleEntries) { + if (entry.getStartTime().isShorterThan(offset) || entry.getStartTime().equals(offset)) { + return new BasalScheduleLookupResult( // + reversedBasalScheduleEntries.size() - (index + 1), // + entry, // + entry.getStartTime(), // + last.minus(entry.getStartTime())); + } + last = entry.getStartTime(); + index++; + } + + throw new IllegalArgumentException("Basal schedule incomplete"); + } + + private List reversedBasalScheduleEntries() { + List reversedEntries = new ArrayList<>(entries); + Collections.reverse(reversedEntries); + return reversedEntries; + } + + public List adjacentEqualRatesMergedEntries() { + List mergedEntries = new ArrayList<>(); + Double lastRate = null; + for (BasalScheduleEntry entry : entries) { + if (lastRate == null || entry.getRate() != lastRate) { + mergedEntries.add(entry); + } + lastRate = entry.getRate(); + } + return mergedEntries; + } + + public List getDurations() { + List durations = new ArrayList<>(); + Duration last = Duration.standardHours(24); + List basalScheduleEntries = reversedBasalScheduleEntries(); + for (BasalScheduleEntry entry : basalScheduleEntries) { + durations.add(new BasalScheduleDurationEntry( // + entry.getRate(), // + entry.getStartTime(), // + last.minus(entry.getStartTime()))); + last = entry.getStartTime(); + } + + Collections.reverse(durations); + return durations; + } + + @Override + public String toString() { + return "BasalSchedule{" + + "entries=" + entries + + '}'; + } + + public static class BasalScheduleDurationEntry { + private final double rate; + private final Duration duration; + private final Duration startTime; + + public BasalScheduleDurationEntry(double rate, Duration startTime, Duration duration) { + this.rate = rate; + this.duration = duration; + this.startTime = startTime; + } + + public double getRate() { + return rate; + } + + public Duration getDuration() { + return duration; + } + + public Duration getStartTime() { + return startTime; + } + } + + public static class BasalScheduleLookupResult { + private final int index; + private final BasalScheduleEntry basalScheduleEntry; + private final Duration startTime; + private final Duration duration; + + public BasalScheduleLookupResult(int index, BasalScheduleEntry basalScheduleEntry, Duration startTime, Duration duration) { + this.index = index; + this.basalScheduleEntry = basalScheduleEntry; + this.startTime = startTime; + this.duration = duration; + } + + public int getIndex() { + return index; + } + + public BasalScheduleEntry getBasalScheduleEntry() { + return basalScheduleEntry; + } + + public Duration getStartTime() { + return startTime; + } + + public Duration getDuration() { + return duration; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java new file mode 100644 index 0000000000..f8065e0f4b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalScheduleEntry { + private final double rate; + private final Duration startTime; + + public BasalScheduleEntry(double rate, Duration startTime) { + if (startTime.isLongerThan(Duration.standardHours(24).minus(Duration.standardSeconds(1))) || startTime.isShorterThan(Duration.ZERO) || startTime.getStandardSeconds() % 1800 != 0) { + throw new IllegalArgumentException("Invalid start time"); + } else if (rate < 0D) { + throw new IllegalArgumentException("Rate should be >= 0"); + } else if (rate > OmnipodConst.MAX_BASAL_RATE) { + throw new IllegalArgumentException("Rate exceeds max basal rate"); + } else if (rate % OmnipodConst.POD_PULSE_SIZE > 0.000001 && rate % OmnipodConst.POD_PULSE_SIZE - OmnipodConst.POD_PULSE_SIZE < -0.000001) { + throw new IllegalArgumentException("Unsupported basal rate precision"); + } + this.rate = rate; + this.startTime = startTime; + } + + public double getRate() { + return rate; + } + + public Duration getStartTime() { + return startTime; + } + + @Override + public String toString() { + return "BasalScheduleEntry{" + + "rate=" + rate + + ", startTime=" + startTime.getStandardSeconds() + "s" + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java new file mode 100644 index 0000000000..55339865a0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class BasalTableEntry implements IRawRepresentable { + + private final int segments; + private final int pulses; + private final boolean alternateSegmentPulse; + + public BasalTableEntry(int segments, int pulses, boolean alternateSegmentPulse) { + this.segments = segments; + this.pulses = pulses; + this.alternateSegmentPulse = alternateSegmentPulse; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[2]; + byte pulsesHighByte = (byte) ((pulses >>> 8) & 0b11); + byte pulsesLowByte = (byte) pulses; + rawData[0] = (byte) ((byte) ((segments - 1) << 4) + (byte) ((alternateSegmentPulse ? 1 : 0) << 3) + pulsesHighByte); + rawData[1] = pulsesLowByte; + return rawData; + } + + public int getChecksum() { + int checksumPerSegment = ByteUtil.convertUnsignedByteToInt((byte) pulses) + (pulses >>> 8); + return (checksumPerSegment * segments + (alternateSegmentPulse ? segments / 2 : 0)); + } + + public int getSegments() { + return this.segments; + } + + public int getPulses() { + return pulses; + } + + public boolean isAlternateSegmentPulse() { + return alternateSegmentPulse; + } + + @Override + public String toString() { + return "BasalTableEntry{" + + "segments=" + segments + + ", pulses=" + pulses + + ", alternateSegmentPulse=" + alternateSegmentPulse + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java new file mode 100644 index 0000000000..167f1bcce5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java @@ -0,0 +1,60 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BolusDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final double units; + private final Duration timeBetweenPulses; + + public BolusDeliverySchedule(double units, Duration timeBetweenPulses) { + if (units <= 0D) { + throw new IllegalArgumentException("Units should be > 0"); + } else if (units > OmnipodConst.MAX_BOLUS) { + throw new IllegalArgumentException("Units exceeds max bolus"); + } + this.units = units; + this.timeBetweenPulses = timeBetweenPulses; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[]{1}; // Number of half hour segments + + int pulseCount = (int) Math.round(units / OmnipodConst.POD_PULSE_SIZE); + int multiplier = (int) timeBetweenPulses.getStandardSeconds() * 8; + int fieldA = pulseCount * multiplier; + + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(fieldA)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulseCount)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulseCount)); + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.BOLUS; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 7; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + return checksum; + } + + @Override + public String toString() { + return "BolusDeliverySchedule{" + + "units=" + units + + ", timeBetweenPulses=" + timeBetweenPulses + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java new file mode 100644 index 0000000000..325b7f19e4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java @@ -0,0 +1,10 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public abstract class DeliverySchedule implements IRawRepresentable { + + public abstract InsulinScheduleType getType(); + + public abstract int getChecksum(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java new file mode 100644 index 0000000000..6c7a364e2d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +public enum InsulinScheduleType { + BASAL_SCHEDULE(0), + TEMP_BASAL_SCHEDULE(1), + BOLUS(2); + + private byte value; + + InsulinScheduleType(int value) { + this.value = (byte) value; + } + + public static InsulinScheduleType fromByte(byte input) { + for (InsulinScheduleType type : values()) { + if (type.value == input) { + return type; + } + } + return null; + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java new file mode 100644 index 0000000000..c9bda11562 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java @@ -0,0 +1,77 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class RateEntry implements IRawRepresentable { + + private final double totalPulses; + // We use a double for the delay between pulses because the Joda time API lacks precision for our calculations + private final double delayBetweenPulsesInSeconds; + + public RateEntry(double totalPulses, double delayBetweenPulsesInSeconds) { + this.totalPulses = totalPulses; + this.delayBetweenPulsesInSeconds = delayBetweenPulsesInSeconds; + } + + public static List createEntries(double rate, Duration duration) { + List entries = new ArrayList<>(); + int remainingSegments = (int) Math.round(duration.getStandardSeconds() / 1800.0); + double pulsesPerSegment = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE) / 2.0; + int maxSegmentsPerEntry = pulsesPerSegment > 0 ? (int) (BasalDeliveryTable.MAX_PULSES_PER_RATE_ENTRY / pulsesPerSegment) : 1; + + double durationInHours = duration.getStandardSeconds() / 3600.0; + + double remainingPulses = rate * durationInHours / OmnipodConst.POD_PULSE_SIZE; + double delayBetweenPulses = 3600 / rate * OmnipodConst.POD_PULSE_SIZE; + + while (remainingSegments > 0) { + if (rate == 0.0) { + entries.add(new RateEntry(0, 30D * 60)); + remainingSegments -= 1; + } else { + int numSegments = Math.min(maxSegmentsPerEntry, (int) Math.round(remainingPulses / pulsesPerSegment)); + double totalPulses = pulsesPerSegment * numSegments; + entries.add(new RateEntry(totalPulses, delayBetweenPulses)); + remainingSegments -= numSegments; + remainingPulses -= totalPulses; + } + } + + return entries; + } + + public double getTotalPulses() { + return totalPulses; + } + + public double getDelayBetweenPulsesInSeconds() { + return delayBetweenPulsesInSeconds; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16((int) Math.round(totalPulses * 10))); + if (totalPulses == 0) { + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt((int) (delayBetweenPulsesInSeconds * 1000 * 1000))); + } else { + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt((int) (delayBetweenPulsesInSeconds * 1000 * 100))); + } + return rawData; + } + + @Override + public String toString() { + return "RateEntry{" + + "totalPulses=" + totalPulses + + ", delayBetweenPulsesInSeconds=" + delayBetweenPulsesInSeconds + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java new file mode 100644 index 0000000000..a4c320b1d3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java @@ -0,0 +1,69 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class TempBasalDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final int secondsRemaining; + private final int firstSegmentPulses; + private final BasalDeliveryTable basalTable; + + public TempBasalDeliverySchedule(int secondsRemaining, int firstSegmentPulses, BasalDeliveryTable basalTable) { + this.secondsRemaining = secondsRemaining; + this.firstSegmentPulses = firstSegmentPulses; + this.basalTable = basalTable; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, basalTable.numSegments()); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(secondsRemaining << 3)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(firstSegmentPulses)); + for (BasalTableEntry entry : basalTable.getEntries()) { + rawData = ByteUtil.concat(rawData, entry.getRawData()); + } + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.TEMP_BASAL_SCHEDULE; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 5; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + for (BasalTableEntry entry : basalTable.getEntries()) { + checksum += entry.getChecksum(); + } + + return checksum; + } + + public int getSecondsRemaining() { + return secondsRemaining; + } + + public int getFirstSegmentPulses() { + return firstSegmentPulses; + } + + public BasalDeliveryTable getBasalTable() { + return basalTable; + } + + @Override + public String toString() { + return "TempBasalDeliverySchedule{" + + "secondsRemaining=" + secondsRemaining + + ", firstSegmentPulses=" + firstSegmentPulses + + ", basalTable=" + basalTable + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java new file mode 100644 index 0000000000..22bd4293fe --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java @@ -0,0 +1,299 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import com.google.gson.Gson; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; + +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FirmwareVersion; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.NonceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.sharedPreferences.SP; + + +public class PodSessionState extends PodState { + + @Inject AAPSLogger aapsLogger; + @Inject SP sp; + @Inject OmnipodUtil omnipodUtil; + + private final Map configuredAlerts; + private transient PodStateChangedHandler stateChangedHandler; + private DateTime activatedAt; + private DateTime expiresAt; + private final FirmwareVersion piVersion; + private final FirmwareVersion pmVersion; + private final int lot; + private final int tid; + private Double reservoirLevel; + private boolean suspended; + + private DateTimeZone timeZone; + private NonceState nonceState; + private SetupProgress setupProgress; + private AlertSet activeAlerts; + private BasalSchedule basalSchedule; + private DeliveryStatus lastDeliveryStatus; + + public PodSessionState(DateTimeZone timeZone, int address, FirmwareVersion piVersion, + FirmwareVersion pmVersion, int lot, int tid, int packetNumber, int messageNumber, HasAndroidInjector injector) { + super(address, messageNumber, packetNumber); + injectDaggerClass(injector); + if (timeZone == null) { + throw new IllegalArgumentException("Time zone can not be null"); + } + + suspended = false; + configuredAlerts = new HashMap<>(); + configuredAlerts.put(AlertSlot.SLOT7, AlertType.FINISH_SETUP_REMINDER); + + this.timeZone = timeZone; + this.setupProgress = SetupProgress.ADDRESS_ASSIGNED; + this.piVersion = piVersion; + this.pmVersion = pmVersion; + this.lot = lot; + this.tid = tid; + this.nonceState = new NonceState(lot, tid); + handleUpdates(); + } + + public void injectDaggerClass(HasAndroidInjector injector) { + injector.androidInjector().inject(this); + } + + public void setStateChangedHandler(PodStateChangedHandler handler) { + // FIXME this is an ugly workaround for not being able to serialize the PodStateChangedHandler + if (stateChangedHandler != null) { + throw new IllegalStateException("A PodStateChangedHandler has already been already registered"); + } + stateChangedHandler = handler; + } + + public AlertType getConfiguredAlertType(AlertSlot alertSlot) { + return configuredAlerts.get(alertSlot); + } + + public void putConfiguredAlert(AlertSlot alertSlot, AlertType alertType) { + configuredAlerts.put(alertSlot, alertType); + handleUpdates(); + } + + public void removeConfiguredAlert(AlertSlot alertSlot) { + configuredAlerts.remove(alertSlot); + handleUpdates(); + } + + public DateTime getActivatedAt() { + return activatedAt == null ? null : activatedAt.withZone(timeZone); + } + + public DateTime getExpiresAt() { + return expiresAt == null ? null : expiresAt.withZone(timeZone); + } + + public String getExpiryDateAsString() { + return expiresAt == null ? "???" : DateUtil.dateAndTimeString(expiresAt.toDate()); + } + + public FirmwareVersion getPiVersion() { + return piVersion; + } + + public FirmwareVersion getPmVersion() { + return pmVersion; + } + + public int getLot() { + return lot; + } + + public int getTid() { + return tid; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public synchronized void resyncNonce(int syncWord, int sentNonce, int sequenceNumber) { + int sum = (sentNonce & 0xFFFF) + + OmniCRC.crc16lookup[sequenceNumber] + + (this.lot & 0xFFFF) + + (this.tid & 0xFFFF); + int seed = ((sum & 0xFFFF) ^ syncWord); + + this.nonceState = new NonceState(lot, tid, (byte) (seed & 0xFF)); + handleUpdates(); + } + + public int getCurrentNonce() { + return nonceState.getCurrentNonce(); + } + + public synchronized void advanceToNextNonce() { + nonceState.advanceToNextNonce(); + handleUpdates(); + } + + public SetupProgress getSetupProgress() { + return setupProgress; + } + + public synchronized void setSetupProgress(SetupProgress setupProgress) { + if (setupProgress == null) { + throw new IllegalArgumentException("Setup state cannot be null"); + } + this.setupProgress = setupProgress; + handleUpdates(); + } + + public boolean isSuspended() { + return suspended; + } + + public boolean hasActiveAlerts() { + return activeAlerts != null && activeAlerts.size() > 0; + } + + public AlertSet getActiveAlerts() { + return activeAlerts; + } + + public DateTimeZone getTimeZone() { + return timeZone; + } + + public void setTimeZone(DateTimeZone timeZone) { + if (timeZone == null) { + throw new IllegalArgumentException("Time zone can not be null"); + } + this.timeZone = timeZone; + handleUpdates(); + } + + public DateTime getTime() { + DateTime now = DateTime.now(); + return now.withZone(timeZone); + } + + public Duration getScheduleOffset() { + DateTime now = getTime(); + DateTime startOfDay = new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), + 0, 0, 0, timeZone); + return new Duration(startOfDay, now); + } + + public boolean hasNonceState() { + return true; + } + + @Override + public void setPacketNumber(int packetNumber) { + super.setPacketNumber(packetNumber); + handleUpdates(); + } + + @Override + public void setMessageNumber(int messageNumber) { + super.setMessageNumber(messageNumber); + handleUpdates(); + } + + public BasalSchedule getBasalSchedule() { + return basalSchedule; + } + + public void setBasalSchedule(BasalSchedule basalSchedule) { + this.basalSchedule = basalSchedule; + handleUpdates(); + } + + public DeliveryStatus getLastDeliveryStatus() { + return lastDeliveryStatus; + } + + @Override + public void setFaultEvent(PodInfoFaultEvent faultEvent) { + super.setFaultEvent(faultEvent); + suspended = true; + handleUpdates(); + } + + @Override + public void updateFromStatusResponse(StatusResponse statusResponse) { + DateTime activatedAtCalculated = getTime().minus(statusResponse.getTimeActive()); + if (activatedAt == null) { + activatedAt = activatedAtCalculated; + } + DateTime expiresAtCalculated = activatedAtCalculated.plus(OmnipodConst.NOMINAL_POD_LIFE); + if (expiresAt == null || expiresAtCalculated.isBefore(expiresAt) || expiresAtCalculated.isAfter(expiresAt.plusMinutes(1))) { + expiresAt = expiresAtCalculated; + } + + boolean newSuspendedState = statusResponse.getDeliveryStatus() == DeliveryStatus.SUSPENDED; + if (suspended != newSuspendedState) { + aapsLogger.info(LTag.PUMPCOMM, "Updating pod suspended state in updateFromStatusResponse. newSuspendedState={}, statusResponse={}", newSuspendedState, statusResponse.toString()); + suspended = newSuspendedState; + } + activeAlerts = statusResponse.getAlerts(); + lastDeliveryStatus = statusResponse.getDeliveryStatus(); + reservoirLevel = statusResponse.getReservoirLevel(); + handleUpdates(); + } + + private void handleUpdates() { + Gson gson = omnipodUtil.getGsonInstance(); + String gsonValue = gson.toJson(this); + aapsLogger.info(LTag.PUMPCOMM, "PodSessionState-SP: Saved Session State to SharedPreferences: " + gsonValue); + sp.putString(OmnipodConst.Prefs.PodState, gsonValue); + if (stateChangedHandler != null) { + stateChangedHandler.handle(this); + } + } + + @Override + public String toString() { + return "PodSessionState{" + + "configuredAlerts=" + configuredAlerts + + ", stateChangedHandler=" + stateChangedHandler + + ", activatedAt=" + activatedAt + + ", expiresAt=" + expiresAt + + ", piVersion=" + piVersion + + ", pmVersion=" + pmVersion + + ", lot=" + lot + + ", tid=" + tid + + ", reservoirLevel=" + reservoirLevel + + ", suspended=" + suspended + + ", timeZone=" + timeZone + + ", nonceState=" + nonceState + + ", setupProgress=" + setupProgress + + ", activeAlerts=" + activeAlerts + + ", basalSchedule=" + basalSchedule + + ", lastDeliveryStatus=" + lastDeliveryStatus + + ", address=" + address + + ", packetNumber=" + packetNumber + + ", messageNumber=" + messageNumber + + ", faultEvent=" + faultEvent + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java new file mode 100644 index 0000000000..26b5802258 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; + +public class PodSetupState extends PodState { + public PodSetupState(int address, int packetNumber, int messageNumber) { + super(address, packetNumber, messageNumber); + } + + @Override + public boolean hasNonceState() { + return false; + } + + @Override + public int getCurrentNonce() { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void advanceToNextNonce() { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void resyncNonce(int syncWord, int sentNonce, int sequenceNumber) { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void updateFromStatusResponse(StatusResponse statusResponse) { + } + + @Override + public String toString() { + return "PodSetupState{" + + "address=" + address + + ", packetNumber=" + packetNumber + + ", messageNumber=" + messageNumber + + ", faultEvent=" + faultEvent + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java new file mode 100644 index 0000000000..2e19de1286 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java @@ -0,0 +1,68 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; + +public abstract class PodState { + protected final int address; + protected int packetNumber; + protected int messageNumber; + + protected PodInfoFaultEvent faultEvent; + + public PodState(int address, int packetNumber, int messageNumber) { + this.address = address; + this.packetNumber = packetNumber; + this.messageNumber = messageNumber; + } + + public abstract boolean hasNonceState(); + + public abstract int getCurrentNonce(); + + public abstract void advanceToNextNonce(); + + public abstract void resyncNonce(int syncWord, int sentNonce, int sequenceNumber); + + public abstract void updateFromStatusResponse(StatusResponse statusResponse); + + public int getAddress() { + return address; + } + + public int getMessageNumber() { + return messageNumber; + } + + public void setMessageNumber(int messageNumber) { + this.messageNumber = messageNumber; + } + + public int getPacketNumber() { + return packetNumber; + } + + public void setPacketNumber(int packetNumber) { + this.packetNumber = packetNumber; + } + + public void increaseMessageNumber(int increment) { + setMessageNumber((messageNumber + increment) & 0b1111); + } + + public void increasePacketNumber(int increment) { + setPacketNumber((packetNumber + increment) & 0b11111); + } + + public boolean hasFaultEvent() { + return faultEvent != null; + } + + public PodInfoFaultEvent getFaultEvent() { + return faultEvent; + } + + public void setFaultEvent(PodInfoFaultEvent faultEvent) { + this.faultEvent = faultEvent; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodStateChangedHandler.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodStateChangedHandler.java new file mode 100644 index 0000000000..6e706131a2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodStateChangedHandler.java @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +@FunctionalInterface +public interface PodStateChangedHandler { + void handle(PodSessionState podState); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodHistoryActivity.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodHistoryActivity.java new file mode 100644 index 0000000000..d98721ff15 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodHistoryActivity.java @@ -0,0 +1,339 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs; + +import android.os.Bundle; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.List; + +import javax.inject.Inject; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.activities.NoSplashAppCompatActivity; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpHistoryEntryGroup; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.utils.ProfileUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistory; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.resources.ResourceHelper; + +public class PodHistoryActivity extends NoSplashAppCompatActivity { + + @Inject AAPSLogger aapsLogger; + @Inject OmnipodUtil omnipodUtil; + @Inject ResourceHelper resourceHelper; + + private Spinner historyTypeSpinner; + private TextView statusView; + private RecyclerView recyclerView; + private LinearLayoutManager linearLayoutManager; + + static TypeList showingType = null; + static PumpHistoryEntryGroup selectedGroup = PumpHistoryEntryGroup.All; + List fullHistoryList = new ArrayList<>(); + List filteredHistoryList = new ArrayList<>(); + + RecyclerViewAdapter recyclerViewAdapter; + boolean manualChange = false; + + List typeListFull; + + + public PodHistoryActivity() { + super(); + } + + + private void prepareData() { + GregorianCalendar gc = new GregorianCalendar(); + gc.add(Calendar.HOUR_OF_DAY, -24); + + MainApp.getDbHelper().getPodHistoryFromTime(gc.getTimeInMillis(), false); + + fullHistoryList.addAll(MainApp.getDbHelper().getPodHistoryFromTime(gc.getTimeInMillis(), true)); + } + + + private void filterHistory(PumpHistoryEntryGroup group) { + + this.filteredHistoryList.clear(); + + aapsLogger.debug(LTag.PUMP, "Items on full list: {}", fullHistoryList.size()); + + if (group == PumpHistoryEntryGroup.All) { + this.filteredHistoryList.addAll(fullHistoryList); + } else { + for (PodHistory pumpHistoryEntry : fullHistoryList) { + if (pumpHistoryEntry.getPodDbEntryType().getGroup() == group) { + this.filteredHistoryList.add(pumpHistoryEntry); + } + } + } + + if (this.recyclerViewAdapter != null) { + this.recyclerViewAdapter.setHistoryList(this.filteredHistoryList); + this.recyclerViewAdapter.notifyDataSetChanged(); + } + + aapsLogger.debug(LTag.PUMP, "Items on filtered list: {}", filteredHistoryList.size()); + } + + + @Override + protected void onResume() { + super.onResume(); + filterHistory(selectedGroup); + setHistoryTypeSpinner(); + } + + + private void setHistoryTypeSpinner() { + this.manualChange = true; + + for (int i = 0; i < typeListFull.size(); i++) { + if (typeListFull.get(i).entryGroup == selectedGroup) { + historyTypeSpinner.setSelection(i); + break; + } + } + + SystemClock.sleep(200); + this.manualChange = false; + } + + + @Override + protected void onPause() { + super.onPause(); + } + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.omnipod_pod_history_activity); + + historyTypeSpinner = findViewById(R.id.omnipod_historytype); + statusView = findViewById(R.id.omnipod_historystatus); + recyclerView = findViewById(R.id.omnipod_history_recyclerview); + recyclerView.setHasFixedSize(true); + + linearLayoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(linearLayoutManager); + + prepareData(); + + recyclerViewAdapter = new RecyclerViewAdapter(filteredHistoryList); + recyclerView.setAdapter(recyclerViewAdapter); + + statusView.setVisibility(View.GONE); + + typeListFull = getTypeList(PumpHistoryEntryGroup.getList()); + + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(this, R.layout.spinner_centered, typeListFull); + historyTypeSpinner.setAdapter(spinnerAdapter); + + historyTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (manualChange) + return; + TypeList selected = (TypeList) historyTypeSpinner.getSelectedItem(); + showingType = selected; + selectedGroup = selected.entryGroup; + filterHistory(selectedGroup); + } + + + @Override + public void onNothingSelected(AdapterView parent) { + if (manualChange) + return; + filterHistory(PumpHistoryEntryGroup.All); + } + }); + + } + + + private List getTypeList(List list) { + + ArrayList typeList = new ArrayList<>(); + + for (PumpHistoryEntryGroup pumpHistoryEntryGroup : list) { + typeList.add(new TypeList(pumpHistoryEntryGroup)); + } + + return typeList; + } + + public static class TypeList { + + PumpHistoryEntryGroup entryGroup; + String name; + + TypeList(PumpHistoryEntryGroup entryGroup) { + this.entryGroup = entryGroup; + this.name = entryGroup.getTranslated(); + } + + @NotNull + @Override + public String toString() { + return name; + } + } + + public class RecyclerViewAdapter extends RecyclerView.Adapter { + + List historyList; + + RecyclerViewAdapter(List historyList) { + this.historyList = historyList; + } + + + public void setHistoryList(List historyList) { + this.historyList = historyList; + Collections.sort(this.historyList); + } + + + @NotNull + @Override + public HistoryViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.omnipod_pod_history_item, // + viewGroup, false); + return new HistoryViewHolder(v); + } + + + @Override + public void onBindViewHolder(@NotNull HistoryViewHolder holder, int position) { + PodHistory record = historyList.get(position); + + if (record != null) { + holder.timeView.setText(record.getDateTimeString()); + holder.typeView.setText(record.getPodDbEntryType().getResourceId()); + setValue(record, holder.valueView); + } + } + + + private void setValue(PodHistory historyEntry, TextView valueView) { + //valueView.setText(""); + + if (historyEntry.isSuccess()) { + switch (historyEntry.getPodDbEntryType()) { + + case SetTemporaryBasal: { + TempBasalPair tempBasalPair = omnipodUtil.getGsonInstance().fromJson(historyEntry.getData(), TempBasalPair.class); + valueView.setText(resourceHelper.gs(R.string.omnipod_cmd_tbr_value, tempBasalPair.getInsulinRate(), tempBasalPair.getDurationMinutes())); + } + break; + + case FillCannulaSetBasalProfile: + case SetBasalSchedule: { + if (historyEntry.getData() != null) { + setProfileValue(historyEntry.getData(), valueView); + } + } + break; + + case SetBolus: { + if (historyEntry.getData().contains(";")) { + String[] splitVal = historyEntry.getData().split(";"); + valueView.setText(resourceHelper.gs(R.string.omnipod_cmd_bolus_value_with_carbs, Double.valueOf(splitVal[0]), Double.valueOf(splitVal[1]))); + } else { + valueView.setText(resourceHelper.gs(R.string.omnipod_cmd_bolus_value, Double.valueOf(historyEntry.getData()))); + } + } + break; + + case GetPodStatus: + case GetPodInfo: + case SetTime: + case PairAndPrime: + case CancelTemporaryBasal: + case CancelTemporaryBasalForce: + case ConfigureAlerts: + case CancelBolus: + case DeactivatePod: + case ResetPodState: + case AcknowledgeAlerts: + case SuspendDelivery: + case ResumeDelivery: + case UnknownEntryType: + default: + valueView.setText(""); + break; + + } + } else { + valueView.setText(historyEntry.getData()); + } + + } + + private void setProfileValue(String data, TextView valueView) { + aapsLogger.debug(LTag.PUMP, "Profile json:\n" + data); + + try { + Profile.ProfileValue[] profileValuesArray = omnipodUtil.getGsonInstance().fromJson(data, Profile.ProfileValue[].class); + valueView.setText(ProfileUtil.getBasalProfilesDisplayable(profileValuesArray, PumpType.Insulet_Omnipod)); + } catch (Exception e) { + aapsLogger.error(LTag.PUMP, "Problem parsing Profile json. Ex: {}, Data:\n{}", e.getMessage(), data); + valueView.setText(""); + } + } + + + @Override + public int getItemCount() { + return historyList.size(); + } + + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + } + + + class HistoryViewHolder extends RecyclerView.ViewHolder { + + TextView timeView; + TextView typeView; + TextView valueView; + + HistoryViewHolder(View itemView) { + super(itemView); + timeView = itemView.findViewById(R.id.omnipod_history_time); + typeView = itemView.findViewById(R.id.omnipod_history_source); + valueView = itemView.findViewById(R.id.omnipod_history_description); + } + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodManagementActivity.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodManagementActivity.kt new file mode 100644 index 0000000000..c3a7bfeccd --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/PodManagementActivity.kt @@ -0,0 +1,170 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs + +import android.content.Intent +import android.os.Bundle +import com.atech.android.library.wizardpager.WizardPagerActivity +import com.atech.android.library.wizardpager.WizardPagerContext +import com.atech.android.library.wizardpager.data.WizardPagerSettings +import com.atech.android.library.wizardpager.defs.WizardStepsWayType +import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.R +import info.nightscout.androidaps.activities.NoSplashAppCompatActivity +import info.nightscout.androidaps.events.EventRefreshOverview +import info.nightscout.androidaps.interfaces.CommandQueueProvider +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.defs.PodActionType +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model.FullInitPodWizardModel +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model.RemovePodWizardModel +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model.ShortInitPodWizardModel +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages.InitPodRefreshAction +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState +import info.nightscout.androidaps.plugins.pump.omnipod.driver.comm.AapsOmnipodManager +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.alertDialogs.OKDialog +import info.nightscout.androidaps.utils.resources.ResourceHelper +import kotlinx.android.synthetic.main.omnipod_pod_mgmt.* +import javax.inject.Inject + +/** + * Created by andy on 30/08/2019 + */ +class PodManagementActivity : NoSplashAppCompatActivity() { + + @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var commandQueue: CommandQueueProvider + @Inject lateinit var omnipodUtil: OmnipodUtil + @Inject lateinit var injector: HasAndroidInjector + + private var initPodChanged = false + private var podSessionFullyInitalized = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.omnipod_pod_mgmt) + + initpod_init_pod.setOnClickListener { + initPodAction() + initPodChanged = true + } + + initpod_remove_pod.setOnClickListener { + removePodAction() + initPodChanged = true + } + + initpod_reset_pod.setOnClickListener { + resetPodAction() + initPodChanged = true + } + + initpod_pod_history.setOnClickListener { + showPodHistory() + } + + refreshButtons(); + } + + override fun onDestroy() { + super.onDestroy() + + if (initPodChanged) { + rxBus.send(EventOmnipodPumpValuesChanged()) + rxBus.send(EventRefreshOverview("Omnipod Pod Management")) + } + } + + fun initPodAction() { + + val pagerSettings = WizardPagerSettings() + var refreshAction = InitPodRefreshAction(injector, this, PodActionType.InitPod) + + pagerSettings.setWizardStepsWayType(WizardStepsWayType.CancelNext) + pagerSettings.setFinishStringResourceId(R.string.close) + pagerSettings.setFinishButtonBackground(R.drawable.finish_background) + pagerSettings.setNextButtonBackground(R.drawable.selectable_item_background) + pagerSettings.setBackStringResourceId(R.string.cancel) + pagerSettings.cancelAction = refreshAction + pagerSettings.finishAction = refreshAction + + val wizardPagerContext = WizardPagerContext.getInstance() + + wizardPagerContext.clearContext() + wizardPagerContext.pagerSettings = pagerSettings + val podSessionState = omnipodUtil.getPodSessionState() + val isFullInit = podSessionState == null || podSessionState.setupProgress.isBefore(SetupProgress.PRIMING_FINISHED) + if (isFullInit) { + wizardPagerContext.wizardModel = FullInitPodWizardModel(applicationContext) + } else { + wizardPagerContext.wizardModel = ShortInitPodWizardModel(applicationContext) + } + + val myIntent = Intent(this@PodManagementActivity, WizardPagerActivity::class.java) + this@PodManagementActivity.startActivity(myIntent) + } + + fun removePodAction() { + val pagerSettings = WizardPagerSettings() + var refreshAction = InitPodRefreshAction(injector, this, PodActionType.RemovePod) + + pagerSettings.setWizardStepsWayType(WizardStepsWayType.CancelNext) + pagerSettings.setFinishStringResourceId(R.string.close) + pagerSettings.setFinishButtonBackground(R.drawable.finish_background) + pagerSettings.setNextButtonBackground(R.drawable.selectable_item_background) + pagerSettings.setBackStringResourceId(R.string.cancel) + pagerSettings.cancelAction = refreshAction + pagerSettings.finishAction = refreshAction + + val wizardPagerContext = WizardPagerContext.getInstance(); + + wizardPagerContext.clearContext() + wizardPagerContext.pagerSettings = pagerSettings + wizardPagerContext.wizardModel = RemovePodWizardModel(applicationContext) + + val myIntent = Intent(this@PodManagementActivity, WizardPagerActivity::class.java) + this@PodManagementActivity.startActivity(myIntent) + + } + + fun resetPodAction() { + OKDialog.showConfirmation(this, + resourceHelper.gs(R.string.omnipod_cmd_reset_pod_desc), Thread { + AapsOmnipodManager.getInstance().resetPodStatus() + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_NoPod) + refreshButtons() + }) + } + + fun showPodHistory() { +// OKDialog.showConfirmation(this, +// MainApp.gs(R.string.omnipod_cmd_pod_history_na), null) + + startActivity(Intent(applicationContext, PodHistoryActivity::class.java)) + } + + fun refreshButtons() { + initpod_init_pod.isEnabled = (omnipodUtil.getPodSessionState() == null || + omnipodUtil.getPodSessionState().getSetupProgress().isBefore(SetupProgress.COMPLETED)) + + val isPodSessionActive = (omnipodUtil.getPodSessionState() != null) + + initpod_remove_pod.isEnabled = isPodSessionActive + initpod_reset_pod.isEnabled = isPodSessionActive + + if (omnipodUtil.getDriverState() == OmnipodDriverState.NotInitalized) { + // if rileylink is not running we disable all operations + initpod_init_pod.isEnabled = false + initpod_remove_pod.isEnabled = false + initpod_reset_pod.isEnabled = false + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/defs/PodActionType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/defs/PodActionType.java new file mode 100644 index 0000000000..674703855a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/defs/PodActionType.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.defs; + +public enum PodActionType { + InitPod, + RemovePod, + ResetPod +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionFragment.java new file mode 100644 index 0000000000..7cb106ca47 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionFragment.java @@ -0,0 +1,241 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod; + +import android.app.Activity; +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.fragment.app.Fragment; + +import com.atech.android.library.wizardpager.util.WizardPagesUtil; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.ui.PageFragmentCallbacks; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; + +/** + * Created by andy on 12/11/2019 + */ +public class InitActionFragment extends Fragment implements PodInitReceiver { + private static final String ARG_KEY = "key"; + + protected PageFragmentCallbacks mCallbacks; + protected String mKey; + protected InitActionPage mPage; + + protected ProgressBar progressBar; + protected TextView errorView; + protected Button retryButton; + + protected PodInitActionType podInitActionType; + protected List children; + protected Map mapCheckBoxes; + protected InitActionFragment instance; + + protected PumpEnactResult callResult; + + + public static InitActionFragment create(String key, PodInitActionType podInitActionType) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + InitActionFragment fragment = new InitActionFragment(); + fragment.setArguments(args); + fragment.setPodInitActionType(podInitActionType); + return fragment; + } + + public InitActionFragment() { + this.instance = this; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + mKey = args.getString(ARG_KEY); + mPage = (InitActionPage) mCallbacks.onGetPage(mKey); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.omnipod_initpod_init_action, container, false); + WizardPagesUtil.setTitle(mPage, rootView); + + this.progressBar = rootView.findViewById(R.id.initAction_progressBar); + this.errorView = rootView.findViewById(R.id.initAction_textErrorMessage); + + TextView headerView = rootView.findViewById(R.id.initAction_header); + + LinearLayout linearLayout = rootView.findViewById(R.id.initAction_ItemsHolder); + + children = podInitActionType.getChildren(); + mapCheckBoxes = new HashMap<>(); + + for (PodInitActionType child : children) { + + CheckBox checkBox1 = new CheckBox(getContext()); + checkBox1.setText(child.getResourceId()); + checkBox1.setClickable(false); + checkBox1.setTextAppearance(R.style.WizardPagePodListItem); + checkBox1.setHeight(120); + checkBox1.setTextSize(15); + checkBox1.setTextColor(headerView.getTextColors().getDefaultColor()); + + linearLayout.addView(checkBox1); + + mapCheckBoxes.put(child, checkBox1); + } + + if (podInitActionType == PodInitActionType.FillCannulaSetBasalProfileWizardStep) { + headerView.setText(R.string.omnipod_init_pod_wizard_step4_action_header); + } else if (podInitActionType == PodInitActionType.DeactivatePodWizardStep) { + headerView.setText(R.string.omnipod_remove_pod_wizard_step2_action_header); + } + + this.retryButton = rootView.findViewById(R.id.initAction_RetryButton); + + this.retryButton.setOnClickListener(view -> { + + getActivity().runOnUiThread(() -> { + for (PodInitActionType actionType : mapCheckBoxes.keySet()) { + mapCheckBoxes.get(actionType).setChecked(false); + mapCheckBoxes.get(actionType).setTextColor(headerView.getTextColors().getDefaultColor()); + } + }); + + new InitPodTask(instance).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }); + + return rootView; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + + public PodInitActionType getPodInitActionType() { + return podInitActionType; + } + + + public void setPodInitActionType(PodInitActionType podInitActionType) { + this.podInitActionType = podInitActionType; + } + + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + //System.out.println("ACTION: setUserVisibleHint="+ isVisibleToUser); + if (isVisibleToUser) { + //System.out.println("ACTION: Visible"); + new InitPodTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + } else { + System.out.println("ACTION: Not visible"); + } + } + + public void actionOnReceiveResponse(String result) { +// System.out.println("ACTION: actionOnReceiveResponse: " + result); +// +// boolean isOk = callResult.success; +// +// progressBar.setVisibility(View.GONE); +// +// if (!isOk) { +// errorView.setVisibility(View.VISIBLE); +// errorView.setText(callResult.comment); +// } +// +// mPage.setActionCompleted(isOk); +// +// mPage.getData().putString(Page.SIMPLE_DATA_KEY, "ddd"); +// mPage.notifyDataChanged(); + } + + @Override + public void returnInitTaskStatus(PodInitActionType podInitActionType, boolean isSuccess, String errorMessage) { + if (podInitActionType.isParent()) { + for (PodInitActionType actionType : mapCheckBoxes.keySet()) { + setCheckBox(actionType, isSuccess); + } + + // special handling for init + processOnFinishedActions(isSuccess, errorMessage); + + } else { + setCheckBox(podInitActionType, isSuccess); + } + } + + + private void processOnFinishedActions(boolean isOk, String errorMessage) { + + getActivity().runOnUiThread(() -> { + + progressBar.setVisibility(View.GONE); + + if (!isOk) { + errorView.setVisibility(View.VISIBLE); + errorView.setText(errorMessage); + + retryButton.setVisibility(View.VISIBLE); + } + + mPage.setActionCompleted(isOk); + + mPage.getData().putString(Page.SIMPLE_DATA_KEY, UUID.randomUUID().toString()); + mPage.notifyDataChanged(); + + }); + + } + + + public void setCheckBox(PodInitActionType podInitActionType, boolean isSuccess) { + getActivity().runOnUiThread(() -> { + mapCheckBoxes.get(podInitActionType).setChecked(isSuccess); + mapCheckBoxes.get(podInitActionType).setTextColor(isSuccess ? Color.rgb(34, 135, 91) : + Color.rgb(168, 36, 15)); + }); + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionPage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionPage.java new file mode 100644 index 0000000000..8b34b99193 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitActionPage.java @@ -0,0 +1,73 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod; + +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.ModelCallbacks; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.ReviewItem; + +import java.util.ArrayList; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; + + +/** + * Created by andy on 12/11/2019 + * + * This page is for InitPod and RemovePod, but Fragments called for this 2 actions are different + */ +public class InitActionPage extends Page { + + protected PodInitActionType podInitActionType; + + protected boolean actionCompleted = false; + protected boolean actionSuccess = false; + + public InitActionPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + public InitActionPage(ModelCallbacks callbacks, @StringRes int titleId, PodInitActionType podInitActionType) { + super(callbacks, titleId); + this.podInitActionType = podInitActionType; + } + + @Override + public Fragment createFragment() { + return InitActionFragment.create(getKey(), this.podInitActionType); + } + + @Override + public void getReviewItems(ArrayList dest) { + } + + @Override + public boolean isCompleted() { + return actionCompleted; + } + + public void setActionCompleted(boolean success) { + this.actionCompleted = success; + this.actionSuccess = success; + } + + /** + * This is used just if we want to override default behavior (for example when we enter Page we want prevent any action, until something happens. + * + * @return + */ + public boolean isBackActionPossible() { + return actionCompleted; + } + + /** + * This is used just if we want to override default behavior (for example when we enter Page we want prevent any action, until something happens. + * + * @return + */ + public boolean isNextActionPossible() { + return actionSuccess; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitPodTask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitPodTask.java new file mode 100644 index 0000000000..32f6eabac4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/initpod/InitPodTask.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod; + +import android.os.AsyncTask; +import android.os.SystemClock; +import android.view.View; + +import javax.inject.Inject; + +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.comm.AapsOmnipodManager; + +/** + * Created by andy on 11/12/2019 + */ +public class InitPodTask extends AsyncTask { + + @Inject ProfileFunction profileFunction; + private InitActionFragment initActionFragment; + + public InitPodTask(InitActionFragment initActionFragment) { + + this.initActionFragment = initActionFragment; + } + + protected void onPreExecute() { + initActionFragment.progressBar.setVisibility(View.VISIBLE); + initActionFragment.errorView.setVisibility(View.GONE); + initActionFragment.retryButton.setVisibility(View.GONE); + } + + @Override + protected String doInBackground(Void... params) { + + if (initActionFragment.podInitActionType == PodInitActionType.PairAndPrimeWizardStep) { + initActionFragment.callResult = AapsOmnipodManager.getInstance().initPod( + initActionFragment.podInitActionType, + initActionFragment.instance, + null + ); + } else if (initActionFragment.podInitActionType == PodInitActionType.FillCannulaSetBasalProfileWizardStep) { + initActionFragment.callResult = AapsOmnipodManager.getInstance().initPod( + initActionFragment.podInitActionType, + initActionFragment.instance, + profileFunction.getProfile() + ); + } else if (initActionFragment.podInitActionType == PodInitActionType.DeactivatePodWizardStep) { + initActionFragment.callResult = AapsOmnipodManager.getInstance().deactivatePod(initActionFragment.instance); + } + + return "OK"; + } + + @Override + protected void onPostExecute(String result) { + super.onPostExecute(result); + + initActionFragment.actionOnReceiveResponse(result); + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/FullInitPodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/FullInitPodWizardModel.java new file mode 100644 index 0000000000..a2e7383091 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/FullInitPodWizardModel.java @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import com.atech.android.library.wizardpager.model.DisplayTextPage; +import com.tech.freak.wizardpager.model.PageList; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionPage; + +/** + * Created by andy on 12/11/2019 + */ +// Full init pod wizard model +// Cannot be merged with ShortInitPodWizardModel, because we can't set any instance variables +// before the onNewRootPageList method is called (which happens in the super constructor) +public class FullInitPodWizardModel extends InitPodWizardModel { + + public FullInitPodWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + return new PageList( + new DisplayTextPage(this, + R.string.omnipod_init_pod_wizard_step1_title, + R.string.omnipod_init_pod_wizard_step1_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("None"), + + new InitActionPage(this, + R.string.omnipod_init_pod_wizard_step2_title, + PodInitActionType.PairAndPrimeWizardStep + ).setRequired(true).setCancelReason("Cancel"), + + new DisplayTextPage(this, + R.string.omnipod_init_pod_wizard_step3_title, + R.string.omnipod_init_pod_wizard_step3_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("Cancel"), + + new InitActionPage(this, + R.string.omnipod_init_pod_wizard_step4_title, + PodInitActionType.FillCannulaSetBasalProfileWizardStep + ).setRequired(true).setCancelReason("Cancel") + ); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/InitPodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/InitPodWizardModel.java new file mode 100644 index 0000000000..f5208a48d8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/InitPodWizardModel.java @@ -0,0 +1,21 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.AbstractWizardModel; + +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages.PodInfoFragment; + +public abstract class InitPodWizardModel extends AbstractWizardModel { + public InitPodWizardModel(Context context) { + super(context); + } + + @Override + public Fragment getReviewFragment() { + PodInfoFragment.isInitPod = true; + return new PodInfoFragment(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/RemovePodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/RemovePodWizardModel.java new file mode 100644 index 0000000000..57a7f81414 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/RemovePodWizardModel.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012 Roman Nurik + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import com.atech.android.library.wizardpager.model.DisplayTextPage; +import com.tech.freak.wizardpager.model.AbstractWizardModel; +import com.tech.freak.wizardpager.model.PageList; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages.PodInfoFragment; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.removepod.RemovePodActionPage; + +/** + * Created by andy on 12/11/2019 + */ +public class RemovePodWizardModel extends AbstractWizardModel { + + public RemovePodWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + + return new PageList( + + new DisplayTextPage(this, + R.string.omnipod_remove_pod_wizard_step1_title, + R.string.omnipod_remove_pod_wizard_step1_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("None"), + + new RemovePodActionPage(this, + R.string.omnipod_remove_pod_wizard_step2_title, + PodInitActionType.DeactivatePodWizardStep + ).setRequired(true).setCancelReason("Cancel") + + ); + } + + + public Fragment getReviewFragment() { + PodInfoFragment.isInitPod = false; + return new PodInfoFragment(); + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/ShortInitPodWizardModel.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/ShortInitPodWizardModel.java new file mode 100644 index 0000000000..5499fdf8ec --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/model/ShortInitPodWizardModel.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.model; + +import android.content.Context; + +import com.atech.android.library.wizardpager.model.DisplayTextPage; +import com.tech.freak.wizardpager.model.PageList; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionPage; + +/** + * Created by andy on 12/11/2019 + */ +// Init pod wizard model without the pair and prime step +// Cannot be merged with FullInitPodWizardModel, because we can't set any instance variables +// before the onNewRootPageList method is called (which happens in the super constructor) +public class ShortInitPodWizardModel extends InitPodWizardModel { + + public ShortInitPodWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + return new PageList( + new DisplayTextPage(this, + R.string.omnipod_init_pod_wizard_step3_title, + R.string.omnipod_init_pod_wizard_step3_desc, + R.style.WizardPagePodContent).setRequired(true).setCancelReason("Cancel"), + + new InitActionPage(this, + R.string.omnipod_init_pod_wizard_step4_title, + PodInitActionType.FillCannulaSetBasalProfileWizardStep + ).setRequired(true).setCancelReason("Cancel") + ); + + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/InitPodRefreshAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/InitPodRefreshAction.java new file mode 100644 index 0000000000..37c6d420f7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/InitPodRefreshAction.java @@ -0,0 +1,100 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages; + +import com.atech.android.library.wizardpager.defs.action.AbstractCancelAction; +import com.atech.android.library.wizardpager.defs.action.FinishActionInterface; + +import org.json.JSONException; +import org.json.JSONObject; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.db.CareportalEvent; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.PodManagementActivity; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.defs.PodActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.sharedPreferences.SP; + + +/** + * Created by andy on 12/11/2019 + */ +public class InitPodRefreshAction extends AbstractCancelAction implements FinishActionInterface { + + private PodManagementActivity podManagementActivity; + private PodActionType actionType; + + @Inject OmnipodUtil omnipodUtil; + @Inject AAPSLogger aapsLogger; + @Inject SP sp; + + public InitPodRefreshAction(HasAndroidInjector injector, PodManagementActivity podManagementActivity, PodActionType actionType) { + injector.androidInjector().inject(this); + this.podManagementActivity = podManagementActivity; + this.actionType = actionType; + } + + @Override + public void execute(String cancelReason) { + if (cancelReason != null && cancelReason.trim().length() > 0) { + this.cancelActionText = cancelReason; + } + + if (this.cancelActionText.equals("Cancel")) { + //AapsOmnipodManager.getInstance().resetPodStatus(); + } + + podManagementActivity.refreshButtons(); + } + + @Override + public void execute() { + if (actionType == PodActionType.InitPod) { + if (omnipodUtil.getPodSessionState().getSetupProgress().isBefore(SetupProgress.COMPLETED)) { + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_PodInitializing); + } else { + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_PodAttached); + uploadCareportalEvent(System.currentTimeMillis(), CareportalEvent.SITECHANGE); + } + } else { + omnipodUtil.setDriverState(OmnipodDriverState.Initalized_NoPod); + } + + podManagementActivity.refreshButtons(); + } + + private void uploadCareportalEvent(long date, String event) { + if (MainApp.getDbHelper().getCareportalEventFromTimestamp(date) != null) + return; + try { + JSONObject data = new JSONObject(); + String enteredBy = sp.getString("careportal_enteredby", ""); + if (!enteredBy.equals("")) data.put("enteredBy", enteredBy); + data.put("created_at", DateUtil.toISOString(date)); + data.put("eventType", event); + CareportalEvent careportalEvent = new CareportalEvent(); + careportalEvent.date = date; + careportalEvent.source = Source.USER; + careportalEvent.eventType = event; + careportalEvent.json = data.toString(); + MainApp.getDbHelper().createOrUpdate(careportalEvent); + NSUpload.uploadCareportalEntryToNS(data); + } catch (JSONException e) { + aapsLogger.error(LTag.PUMPCOMM, "Unhandled exception when uploading SiteChange event.", e); + } + } + + + @Override + public String getFinishActionText() { + return "Finish_OK"; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoFragment.java new file mode 100644 index 0000000000..65d032739d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoFragment.java @@ -0,0 +1,189 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.tech.freak.wizardpager.model.ReviewItem; +import com.tech.freak.wizardpager.ui.PageFragmentCallbacks; + +import java.util.ArrayList; + +import javax.inject.Inject; + +import dagger.android.support.DaggerFragment; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + + +/** + * Created by andy on 12/11/2019 + */ +public class PodInfoFragment extends DaggerFragment { + private static final String ARG_KEY = "key"; + + @Inject OmnipodUtil omnipodUtil; + + private PageFragmentCallbacks mCallbacks; + private String mKey; + private PodInfoPage mPage; + public static boolean isInitPod = false; + private ArrayList mCurrentReviewItems; + + public static PodInfoFragment create(String key, boolean initPod) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + isInitPod = initPod; + + PodInfoFragment fragment = new PodInfoFragment(); + fragment.setArguments(args); + return fragment; + } + + public PodInfoFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.omnipod_initpod_pod_info, container, false); + + TextView titleView = (TextView) rootView.findViewById(R.id.podInfoTitle); + titleView.setText(R.string.omnipod_init_pod_wizard_pod_info_title); + titleView.setTextColor(getResources().getColor(com.tech.freak.wizardpager.R.color.review_green)); + + TextView headerText = rootView.findViewById(R.id.podInfoText); + headerText.setText(isInitPod ? // + R.string.omnipod_init_pod_wizard_pod_info_init_pod_description : // + R.string.omnipod_init_pod_wizard_pod_info_remove_pod_description); + + + if (isInitPod) { + if (createDataOfPod()) { + + ListView listView = (ListView) rootView.findViewById(R.id.podInfoList); + listView.setAdapter(new PodInfoAdapter(mCurrentReviewItems, getContext())); + listView.setChoiceMode(ListView.CHOICE_MODE_NONE); + } + } + + + return rootView; + } + + private boolean createDataOfPod() { + + PodSessionState podSessionState = omnipodUtil.getPodSessionState(); + +// PodSessionState podSessionState = new PodSessionState(DateTimeZone.UTC, +// 483748738, +// new DateTime(), +// new FirmwareVersion(1,0,0), +// new FirmwareVersion(1,0,0), +// 574875, +// 5487584, +// 1, +// 1 +// ); + + if (podSessionState == null) + return false; + + mCurrentReviewItems = new ArrayList<>(); + mCurrentReviewItems.add(new ReviewItem("Pod Address", "" + podSessionState.getAddress(), "33")); + mCurrentReviewItems.add(new ReviewItem("Activated At", podSessionState.getActivatedAt().toString("dd.MM.yyyy HH:mm:ss"), "34")); + mCurrentReviewItems.add(new ReviewItem("Firmware Version", podSessionState.getPiVersion().toString(), "35")); + mCurrentReviewItems.add(new ReviewItem("LOT", "" + podSessionState.getLot(), "36")); + + return true; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + + private class PodInfoAdapter extends ArrayAdapter { + + private ArrayList dataSet; + Context mContext; + private int lastPosition = -1; + + // View lookup cache + + public PodInfoAdapter(ArrayList data, Context context) { + super(context, com.tech.freak.wizardpager.R.layout.list_item_review, data); + this.dataSet = data; + this.mContext = context; + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // Get the data item for this position + ReviewItem dataModel = getItem(position); + // Check if an existing view is being reused, otherwise inflate the view + ViewHolder viewHolder; // view lookup cache stored in tag + + final View result; + + if (convertView == null) { + + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(getContext()); + convertView = inflater.inflate(R.layout.omnipod_initpod_pod_info_item, parent, false); + viewHolder.txtName = (TextView) convertView.findViewById(android.R.id.text1); + viewHolder.txtType = (TextView) convertView.findViewById(android.R.id.text2); + + result = convertView; + + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + result = convertView; + } + + viewHolder.txtName.setText(dataModel.getTitle()); + viewHolder.txtType.setText(dataModel.getDisplayValue()); + + // Return the completed view to render on screen + return convertView; + } + } + + private static class ViewHolder { + TextView txtName; + TextView txtType; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoPage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoPage.java new file mode 100644 index 0000000000..04ca08a0f3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/pages/PodInfoPage.java @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.pages; + +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.ModelCallbacks; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.ReviewItem; + +import java.util.ArrayList; + + +/** + * Created by andy on 12/11/2019 + */ +public class PodInfoPage extends Page { + + public PodInfoPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return PodInfoFragment.create(getKey(), true); + } + + @Override + public void getReviewItems(ArrayList dest) { + } + + @Override + public boolean isCompleted() { + return true; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemoveActionFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemoveActionFragment.java new file mode 100644 index 0000000000..2531185697 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemoveActionFragment.java @@ -0,0 +1,72 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.removepod; + +import android.os.Bundle; +import android.view.View; + +import com.tech.freak.wizardpager.model.Page; + +import java.util.UUID; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionFragment; + +/** + * Created by andy on 29/11/2019 + */ +public class RemoveActionFragment extends InitActionFragment implements PodInitReceiver { + private static final String ARG_KEY = "key"; + + public static RemoveActionFragment create(String key, PodInitActionType podInitActionType) { + Bundle args = new Bundle(); + args.putString(ARG_KEY, key); + + RemoveActionFragment fragment = new RemoveActionFragment(); + fragment.setArguments(args); + fragment.setPodInitActionType(podInitActionType); + return fragment; + } + + public RemoveActionFragment() { + this.instance = this; + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + } + + public void actionOnReceiveResponse(String result) { + System.out.println("ACTION: actionOnReceiveResponse: " + result); + + boolean isOk = callResult.success; + + progressBar.setVisibility(View.GONE); + + if (!isOk) { + errorView.setVisibility(View.VISIBLE); + errorView.setText(callResult.comment); + + retryButton.setVisibility(View.VISIBLE); + } + + mPage.setActionCompleted(isOk); + + mPage.getData().putString(Page.SIMPLE_DATA_KEY, UUID.randomUUID().toString()); + mPage.notifyDataChanged(); + } + + + @Override + public void returnInitTaskStatus(PodInitActionType podInitActionType, boolean isSuccess, String errorMessage) { + if (podInitActionType.isParent()) { + for (PodInitActionType actionType : mapCheckBoxes.keySet()) { + setCheckBox(actionType, isSuccess); + } + } else { + setCheckBox(podInitActionType, isSuccess); + } + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemovePodActionPage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemovePodActionPage.java new file mode 100644 index 0000000000..7f5b2b8503 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dialogs/wizard/removepod/RemovePodActionPage.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.removepod; + +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import com.tech.freak.wizardpager.model.ModelCallbacks; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.dialogs.wizard.initpod.InitActionPage; + + +/** + * Created by andy on 12/11/2019 + * + */ +public class RemovePodActionPage extends InitActionPage { + + public RemovePodActionPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + public RemovePodActionPage(ModelCallbacks callbacks, @StringRes int titleId, PodInitActionType podInitActionType) { + super(callbacks, titleId, podInitActionType); + } + + @Override + public Fragment createFragment() { + return RemoveActionFragment.create(getKey(), this.podInitActionType); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodDriverState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodDriverState.java new file mode 100644 index 0000000000..c2910dab89 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodDriverState.java @@ -0,0 +1,10 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver; + +public enum OmnipodDriverState { + + NotInitalized, // when we start + Initalized_NoPod, // driver is initalized, but there is no pod + Initalized_PodInitializing, // driver is initalized, pod is initalizing + Initalized_PodAttached, // driver is initalized, pod is there + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodPumpStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodPumpStatus.java new file mode 100644 index 0000000000..1262304203 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/OmnipodPumpStatus.java @@ -0,0 +1,193 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver; + +import java.util.Arrays; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.common.data.PumpStatus; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.RLHistoryItem; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkError; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkTargetDevice; +import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; +import info.nightscout.androidaps.plugins.pump.medtronic.events.EventMedtronicDeviceStatusChange; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.sharedPreferences.SP; + +/** + * Created by andy on 4.8.2019 + */ +@Singleton +public class OmnipodPumpStatus extends PumpStatus { + + private final ResourceHelper resourceHelper; + private final SP sp; + private final RileyLinkUtil rileyLinkUtil; + private final RxBusWrapper rxBus; + + public String errorDescription = null; + public String rileyLinkAddress = null; + public boolean inPreInit = true; + + // statuses + public RileyLinkServiceState rileyLinkServiceState = RileyLinkServiceState.NotStarted; + public RileyLinkError rileyLinkError; + public double currentBasal = 0; + public long tempBasalStart; + public long tempBasalEnd; + public Double tempBasalAmount = 0.0d; + public Integer tempBasalLength; + public long tempBasalPumpId; + public PodSessionState podSessionState; + public PumpType pumpType; + + public String regexMac = "([\\da-fA-F]{1,2}(?:\\:|$)){6}"; + + public String podNumber; + public PodDeviceState podDeviceState = PodDeviceState.NeverContacted; + public boolean podAvailable = false; + public boolean podAvailibityChecked = false; + public boolean ackAlertsAvailable = false; + public String ackAlertsText = null; + + public boolean beepBolusEnabled = true; + public boolean beepBasalEnabled = true; + public boolean beepSMBEnabled = true; + public boolean beepTBREnabled = true; + public boolean podDebuggingOptionsEnabled = false; + public String podLotNumber = "???"; + public boolean timeChangeEventEnabled = true; + + public OmnipodDriverState driverState = OmnipodDriverState.NotInitalized; + private PumpDeviceState pumpDeviceState; + + @Inject + public OmnipodPumpStatus(ResourceHelper resourceHelper, + info.nightscout.androidaps.utils.sharedPreferences.SP sp, + RxBusWrapper rxBus, + RileyLinkUtil rileyLinkUtil) { + super(PumpType.Insulet_Omnipod); + this.resourceHelper = resourceHelper; + this.sp = sp; + this.rxBus = rxBus; + this.rileyLinkUtil = rileyLinkUtil; + initSettings(); + } + + + @Override + public void initSettings() { + this.activeProfileName = ""; + this.reservoirRemainingUnits = 75d; + this.batteryRemaining = 75; + this.lastConnection = sp.getLong(OmnipodConst.Statistics.LastGoodPumpCommunicationTime, 0L); + this.lastDataTime = this.lastConnection; + this.pumpType = PumpType.Insulet_Omnipod; + this.podAvailable = false; + } + + + public String getErrorInfo() { + //verifyConfiguration(); + + return (this.errorDescription == null) ? "-" : this.errorDescription; + } + + +// public boolean setNotInPreInit() { +// this.inPreInit = false; +// +// return reconfigureService(); +// } + + + public void clearTemporaryBasal() { + this.tempBasalStart = 0L; + this.tempBasalEnd = 0L; + this.tempBasalAmount = 0.0d; + this.tempBasalLength = 0; + } + + + public TempBasalPair getTemporaryBasal() { + + TempBasalPair tbr = new TempBasalPair(); + tbr.setDurationMinutes(tempBasalLength); + tbr.setInsulinRate(tempBasalAmount); + tbr.setStartTime(tempBasalStart); + tbr.setEndTime(tempBasalEnd); + + return tbr; + } + + @Override + public String toString() { + return "OmnipodPumpStatus{" + + "errorDescription='" + errorDescription + '\'' + + ", rileyLinkAddress='" + rileyLinkAddress + '\'' + + ", inPreInit=" + inPreInit + + ", rileyLinkServiceState=" + rileyLinkServiceState + + ", rileyLinkError=" + rileyLinkError + + ", currentBasal=" + currentBasal + + ", tempBasalStart=" + tempBasalStart + + ", tempBasalEnd=" + tempBasalEnd + + ", tempBasalAmount=" + tempBasalAmount + + ", tempBasalLength=" + tempBasalLength + + ", podSessionState=" + podSessionState + + ", regexMac='" + regexMac + '\'' + + ", podNumber='" + podNumber + '\'' + + ", podDeviceState=" + podDeviceState + + ", podAvailable=" + podAvailable + + ", ackAlertsAvailable=" + ackAlertsAvailable + + ", ackAlertsText='" + ackAlertsText + '\'' + + ", lastDataTime=" + lastDataTime + + ", lastConnection=" + lastConnection + + ", previousConnection=" + previousConnection + + ", lastBolusTime=" + lastBolusTime + + ", lastBolusAmount=" + lastBolusAmount + + ", activeProfileName='" + activeProfileName + '\'' + + ", reservoirRemainingUnits=" + reservoirRemainingUnits + + ", reservoirFullUnits=" + reservoirFullUnits + + ", batteryRemaining=" + batteryRemaining + + ", batteryVoltage=" + batteryVoltage + + ", iob='" + iob + '\'' + + ", dailyTotalUnits=" + dailyTotalUnits + + ", maxDailyTotalUnits='" + maxDailyTotalUnits + '\'' + + ", validBasalRateProfileSelectedOnPump=" + validBasalRateProfileSelectedOnPump + + ", pumpType=" + pumpType + + ", profileStore=" + profileStore + + ", units='" + units + '\'' + + ", pumpStatusType=" + pumpStatusType + + ", basalsByHour=" + Arrays.toString(basalsByHour) + + ", currentBasal=" + currentBasal + + ", tempBasalInProgress=" + tempBasalInProgress + + ", tempBasalRatio=" + tempBasalRatio + + ", tempBasalRemainMin=" + tempBasalRemainMin + + ", tempBasalStart=" + tempBasalStart + + ", pumpType=" + pumpType + + "} "; + } + + + public PumpDeviceState getPumpDeviceState() { + return pumpDeviceState; + } + + + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.pumpDeviceState = pumpDeviceState; + + rileyLinkUtil.getRileyLinkHistory().add(new RLHistoryItem(pumpDeviceState, RileyLinkTargetDevice.Omnipod)); + + rxBus.send(new EventMedtronicDeviceStatusChange(pumpDeviceState)); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManager.java new file mode 100644 index 0000000000..f9fdcf8a83 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/comm/AapsOmnipodManager.java @@ -0,0 +1,757 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.comm; + +import android.content.Intent; +import android.text.TextUtils; + +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.activities.ErrorHelperActivity; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TemporaryBasal; +import info.nightscout.androidaps.events.Event; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.interfaces.TreatmentsInterface; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification; +import info.nightscout.androidaps.plugins.general.overview.events.EventOverviewBolusProgress; +import info.nightscout.androidaps.plugins.general.overview.notifications.Notification; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpStatusType; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodManager; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.SetupActionResult; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.ActionInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommandInitializationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CommunicationException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalDeliveryStatusException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPacketTypeException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalPodProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.IllegalSetupProgressException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.MessageDecodingException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceOutOfSyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NonceResyncException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodFaultException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.exception.PodReturnedErrorResponseException; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentPulseLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalScheduleEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistory; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.db.PodHistoryEntryType; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodAcknowledgeAlertsChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.sharedPreferences.SP; +import io.reactivex.disposables.Disposable; + +public class AapsOmnipodManager implements OmnipodCommunicationManagerInterface { + + private OmnipodUtil omnipodUtil; + private AAPSLogger aapsLogger; + private RxBusWrapper rxBus; + private ResourceHelper resourceHelper; + private HasAndroidInjector injector; + private ActivePluginProvider activePlugin; + private OmnipodPumpStatus pumpStatus; + + private final OmnipodManager delegate; + + private static AapsOmnipodManager instance; + + private Date lastBolusTime; + private Double lastBolusUnits; + + public static AapsOmnipodManager getInstance() { + return instance; + } + + public AapsOmnipodManager(OmnipodCommunicationManager communicationService, + PodSessionState podState, + OmnipodPumpStatus _pumpStatus, + OmnipodUtil omnipodUtil, + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + SP sp, + ResourceHelper resourceHelper, + HasAndroidInjector injector, + ActivePluginProvider activePlugin) { + this.omnipodUtil = omnipodUtil; + this.aapsLogger = aapsLogger; + this.rxBus = rxBus; + this.resourceHelper = resourceHelper; + this.injector = injector; + this.activePlugin = activePlugin; + this.pumpStatus = _pumpStatus; + + delegate = new OmnipodManager(aapsLogger, sp, communicationService, podState, podSessionState -> { + // Handle pod state changes + omnipodUtil.setPodSessionState(podSessionState); + updatePumpStatus(podSessionState); + }); + instance = this; + } + + private void updatePumpStatus(PodSessionState podSessionState) { + if (pumpStatus != null) { + if (podSessionState == null) { + pumpStatus.ackAlertsText = null; + pumpStatus.ackAlertsAvailable = false; + pumpStatus.lastBolusTime = null; + pumpStatus.lastBolusAmount = null; + pumpStatus.reservoirRemainingUnits = 0.0; + pumpStatus.pumpStatusType = PumpStatusType.Suspended; + sendEvent(new EventOmnipodAcknowledgeAlertsChanged()); + sendEvent(new EventOmnipodPumpValuesChanged()); + sendEvent(new EventRefreshOverview("Omnipod Pump", false)); + } else { + // Update active alerts + if (podSessionState.hasActiveAlerts()) { + List alerts = translateActiveAlerts(podSessionState); + String alertsText = TextUtils.join("\n", alerts); + + if (!pumpStatus.ackAlertsAvailable || !alertsText.equals(pumpStatus.ackAlertsText)) { + pumpStatus.ackAlertsAvailable = true; + pumpStatus.ackAlertsText = TextUtils.join("\n", alerts); + + sendEvent(new EventOmnipodAcknowledgeAlertsChanged()); + } + } else { + if (pumpStatus.ackAlertsAvailable || StringUtils.isNotEmpty(pumpStatus.ackAlertsText)) { + pumpStatus.ackAlertsText = null; + pumpStatus.ackAlertsAvailable = false; + sendEvent(new EventOmnipodAcknowledgeAlertsChanged()); + } + } + + // Update other info: last bolus, units remaining, suspended + if (!Objects.equals(lastBolusTime, pumpStatus.lastBolusTime) // + || !Objects.equals(lastBolusUnits, pumpStatus.lastBolusAmount) // + || !isReservoirStatusUpToDate(pumpStatus, podSessionState.getReservoirLevel()) + || podSessionState.isSuspended() != PumpStatusType.Suspended.equals(pumpStatus.pumpStatusType)) { + pumpStatus.lastBolusTime = lastBolusTime; + pumpStatus.lastBolusAmount = lastBolusUnits; + pumpStatus.reservoirRemainingUnits = podSessionState.getReservoirLevel() == null ? 75.0 : podSessionState.getReservoirLevel(); + pumpStatus.pumpStatusType = podSessionState.isSuspended() ? PumpStatusType.Suspended : PumpStatusType.Running; + sendEvent(new EventOmnipodPumpValuesChanged()); + + if (podSessionState.isSuspended() != PumpStatusType.Suspended.equals(pumpStatus.pumpStatusType)) { + sendEvent(new EventRefreshOverview("Omnipod Pump", false)); + } + } + } + } + } + + private static boolean isReservoirStatusUpToDate(OmnipodPumpStatus pumpStatus, Double unitsRemaining) { + double expectedUnitsRemaining = unitsRemaining == null ? 75.0 : unitsRemaining; + return Math.abs(expectedUnitsRemaining - pumpStatus.reservoirRemainingUnits) < 0.000001; + } + + private List translateActiveAlerts(PodSessionState podSessionState) { + List alerts = new ArrayList<>(); + for (AlertSlot alertSlot : podSessionState.getActiveAlerts().getAlertSlots()) { + alerts.add(translateAlertType(podSessionState.getConfiguredAlertType(alertSlot))); + } + return alerts; + } + + @Override + public PumpEnactResult initPod(PodInitActionType podInitActionType, PodInitReceiver podInitReceiver, Profile profile) { + long time = System.currentTimeMillis(); + if (PodInitActionType.PairAndPrimeWizardStep.equals(podInitActionType)) { + try { + Disposable disposable = delegate.pairAndPrime().subscribe(res -> // + handleSetupActionResult(podInitActionType, podInitReceiver, res, time, null)); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + podInitReceiver.returnInitTaskStatus(podInitActionType, false, comment); + addFailureToHistory(time, PodHistoryEntryType.PairAndPrime, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } else if (PodInitActionType.FillCannulaSetBasalProfileWizardStep.equals(podInitActionType)) { + try { + BasalSchedule basalSchedule; + try { + basalSchedule = mapProfileToBasalSchedule(profile); + } catch (Exception ex) { + throw new CommandInitializationException("Basal profile mapping failed", ex); + } + Disposable disposable = delegate.insertCannula(basalSchedule).subscribe(res -> // + handleSetupActionResult(podInitActionType, podInitReceiver, res, time, profile)); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + podInitReceiver.returnInitTaskStatus(podInitActionType, false, comment); + addFailureToHistory(time, PodHistoryEntryType.FillCannulaSetBasalProfile, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } + + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_illegal_init_action_type, podInitActionType.name())); + } + + @Override + public PumpEnactResult getPodStatus() { + long time = System.currentTimeMillis(); + try { + StatusResponse statusResponse = delegate.getPodStatus(); + addSuccessToHistory(time, PodHistoryEntryType.GetPodStatus, statusResponse); + return new PumpEnactResult(injector).success(true).enacted(false); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.GetPodStatus, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } + + @Override + public PumpEnactResult deactivatePod(PodInitReceiver podInitReceiver) { + long time = System.currentTimeMillis(); + try { + delegate.deactivatePod(); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + podInitReceiver.returnInitTaskStatus(PodInitActionType.DeactivatePodWizardStep, false, comment); + addFailureToHistory(time, PodHistoryEntryType.DeactivatePod, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + reportImplicitlyCanceledTbr(); + + addSuccessToHistory(time, PodHistoryEntryType.DeactivatePod, null); + + podInitReceiver.returnInitTaskStatus(PodInitActionType.DeactivatePodWizardStep, true, null); + + this.omnipodUtil.setPodSessionState(null); + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult setBasalProfile(Profile profile) { + long time = System.currentTimeMillis(); + try { + BasalSchedule basalSchedule; + try { + basalSchedule = mapProfileToBasalSchedule(profile); + } catch (Exception ex) { + throw new CommandInitializationException("Basal profile mapping failed", ex); + } + delegate.setBasalSchedule(basalSchedule, isBasalBeepsEnabled()); + // Because setting a basal profile actually suspends and then resumes delivery, TBR is implicitly cancelled + reportImplicitlyCanceledTbr(); + addSuccessToHistory(time, PodHistoryEntryType.SetBasalSchedule, profile.getBasalValues()); + } catch (Exception ex) { + if ((ex instanceof OmnipodException) && !((OmnipodException) ex).isCertainFailure()) { + reportImplicitlyCanceledTbr(); + addToHistory(time, PodHistoryEntryType.SetBasalSchedule, "Uncertain failure", false); + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_set_basal_failed_uncertain)); + } + String comment = handleAndTranslateException(ex); + reportImplicitlyCanceledTbr(); + addFailureToHistory(time, PodHistoryEntryType.SetBasalSchedule, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult resetPodStatus() { + delegate.resetPodState(true); + + reportImplicitlyCanceledTbr(); + + this.omnipodUtil.setPodSessionState(null); + + addSuccessToHistory(System.currentTimeMillis(), PodHistoryEntryType.ResetPodState, null); + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult setBolus(DetailedBolusInfo detailedBolusInfo) { + OmnipodManager.BolusCommandResult bolusCommandResult; + + boolean beepsEnabled = detailedBolusInfo.isSMB ? isSmbBeepsEnabled() : isBolusBeepsEnabled(); + + Date bolusStarted; + try { + bolusCommandResult = delegate.bolus(PumpType.Insulet_Omnipod.determineCorrectBolusSize(detailedBolusInfo.insulin), beepsEnabled, beepsEnabled, detailedBolusInfo.isSMB ? null : + (estimatedUnitsDelivered, percentage) -> { + EventOverviewBolusProgress progressUpdateEvent = EventOverviewBolusProgress.INSTANCE; + progressUpdateEvent.setStatus(getStringResource(R.string.bolusdelivering, detailedBolusInfo.insulin)); + progressUpdateEvent.setPercent(percentage); + sendEvent(progressUpdateEvent); + }); + + bolusStarted = new Date(); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(System.currentTimeMillis(), PodHistoryEntryType.SetBolus, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + if (OmnipodManager.CommandDeliveryStatus.UNCERTAIN_FAILURE.equals(bolusCommandResult.getCommandDeliveryStatus())) { + // For safety reasons, we treat this as a bolus that has successfully been delivered, in order to prevent insulin overdose + + showErrorDialog(getStringResource(R.string.omnipod_bolus_failed_uncertain), R.raw.boluserror); + } + + // Wait for the bolus to finish + OmnipodManager.BolusDeliveryResult bolusDeliveryResult = + bolusCommandResult.getDeliveryResultSubject().blockingGet(); + + double unitsDelivered = bolusDeliveryResult.getUnitsDelivered(); + + if (pumpStatus != null && !detailedBolusInfo.isSMB) { + lastBolusTime = pumpStatus.lastBolusTime = bolusStarted; + lastBolusUnits = pumpStatus.lastBolusAmount = unitsDelivered; + } + + long pumpId = addSuccessToHistory(bolusStarted.getTime(), PodHistoryEntryType.SetBolus, unitsDelivered + ";" + detailedBolusInfo.carbs); + + detailedBolusInfo.date = bolusStarted.getTime(); + detailedBolusInfo.insulin = unitsDelivered; + detailedBolusInfo.pumpId = pumpId; + detailedBolusInfo.source = Source.PUMP; + + activePlugin.getActiveTreatments().addToHistoryTreatment(detailedBolusInfo, false); + + if (delegate.getPodState().hasFaultEvent()) { + showPodFaultErrorDialog(delegate.getPodState().getFaultEvent().getFaultEventType(), R.raw.urgentalarm); + } + + return new PumpEnactResult(injector).success(true).enacted(true).bolusDelivered(unitsDelivered); + } + + @Override + public PumpEnactResult cancelBolus() { + long time = System.currentTimeMillis(); + String comment = null; + while (delegate.hasActiveBolus()) { + try { + delegate.cancelBolus(isBolusBeepsEnabled()); + addSuccessToHistory(time, PodHistoryEntryType.CancelBolus, null); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (PodFaultException ex) { + showPodFaultErrorDialog(ex.getFaultEvent().getFaultEventType(), null); + addSuccessToHistory(time, PodHistoryEntryType.CancelBolus, null); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + comment = handleAndTranslateException(ex); + } + } + + addFailureToHistory(time, PodHistoryEntryType.CancelBolus, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + @Override + public PumpEnactResult setTemporaryBasal(TempBasalPair tempBasalPair) { + boolean beepsEnabled = isTempBasalBeepsEnabled(); + long time = System.currentTimeMillis(); + try { + delegate.setTemporaryBasal(PumpType.Insulet_Omnipod.determineCorrectBasalSize(tempBasalPair.getInsulinRate()), Duration.standardMinutes(tempBasalPair.getDurationMinutes()), beepsEnabled, beepsEnabled); + time = System.currentTimeMillis(); + } catch (Exception ex) { + if ((ex instanceof OmnipodException) && !((OmnipodException) ex).isCertainFailure()) { + addToHistory(time, PodHistoryEntryType.SetTemporaryBasal, "Uncertain failure", false); + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_set_temp_basal_failed_uncertain)); + } + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.SetTemporaryBasal, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + reportImplicitlyCanceledTbr(); + + long pumpId = addSuccessToHistory(time, PodHistoryEntryType.SetTemporaryBasal, tempBasalPair); + + pumpStatus.tempBasalStart = time; + pumpStatus.tempBasalAmount = tempBasalPair.getInsulinRate(); + pumpStatus.tempBasalLength = tempBasalPair.getDurationMinutes(); + pumpStatus.tempBasalEnd = DateTimeUtil.getTimeInFutureFromMinutes(time, tempBasalPair.getDurationMinutes()); + pumpStatus.tempBasalPumpId = pumpId; + + TemporaryBasal tempStart = new TemporaryBasal() // + .date(time) // + .duration(tempBasalPair.getDurationMinutes()) // + .absolute(tempBasalPair.getInsulinRate()) // + .pumpId(pumpId) + .source(Source.PUMP); + + activePlugin.getActiveTreatments().addToHistoryTempBasal(tempStart); + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult cancelTemporaryBasal() { + long time = System.currentTimeMillis(); + try { + delegate.cancelTemporaryBasal(isTempBasalBeepsEnabled()); + addSuccessToHistory(time, PodHistoryEntryType.CancelTemporaryBasalForce, null); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.CancelTemporaryBasalForce, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public PumpEnactResult acknowledgeAlerts() { + long time = System.currentTimeMillis(); + try { + delegate.acknowledgeAlerts(); + addSuccessToHistory(time, PodHistoryEntryType.AcknowledgeAlerts, null); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.AcknowledgeAlerts, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + return new PumpEnactResult(injector).success(true).enacted(true); + } + + @Override + public void setPumpStatus(OmnipodPumpStatus pumpStatus) { + this.pumpStatus = pumpStatus; + updatePumpStatus(delegate.getPodState()); + } + + // TODO should we add this to the OmnipodCommunicationManager interface? + public PumpEnactResult getPodInfo(PodInfoType podInfoType) { + long time = System.currentTimeMillis(); + try { + // TODO how can we return the PodInfo response? + // This method is useless unless we return the PodInfoResponse, + // because the pod state we keep, doesn't get updated from a PodInfoResponse. + // We use StatusResponses for that, which can be obtained from the getPodStatus method + PodInfoResponse podInfo = delegate.getPodInfo(podInfoType); + addSuccessToHistory(time, PodHistoryEntryType.GetPodInfo, podInfo); + return new PumpEnactResult(injector).success(true).enacted(true); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + addFailureToHistory(time, PodHistoryEntryType.GetPodInfo, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + } + + public PumpEnactResult suspendDelivery() { + try { + delegate.suspendDelivery(isBasalBeepsEnabled()); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + public PumpEnactResult resumeDelivery() { + try { + delegate.resumeDelivery(isBasalBeepsEnabled()); + } catch (Exception ex) { + String comment = handleAndTranslateException(ex); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + // TODO should we add this to the OmnipodCommunicationManager interface? + // Updates the pods current time based on the device timezone and the pod's time zone + public PumpEnactResult setTime() { + long time = System.currentTimeMillis(); + try { + delegate.setTime(isBasalBeepsEnabled()); + // Because set time actually suspends and then resumes delivery, TBR is implicitly cancelled + reportImplicitlyCanceledTbr(); + addSuccessToHistory(time, PodHistoryEntryType.SetTime, null); + } catch (Exception ex) { + if ((ex instanceof OmnipodException) && !((OmnipodException) ex).isCertainFailure()) { + reportImplicitlyCanceledTbr(); + addFailureToHistory(time, PodHistoryEntryType.SetTime, "Uncertain failure"); + return new PumpEnactResult(injector).success(false).enacted(false).comment(getStringResource(R.string.omnipod_error_set_time_failed_uncertain)); + } + String comment = handleAndTranslateException(ex); + reportImplicitlyCanceledTbr(); + addFailureToHistory(time, PodHistoryEntryType.SetTime, comment); + return new PumpEnactResult(injector).success(false).enacted(false).comment(comment); + } + + return new PumpEnactResult(injector).success(true).enacted(true); + } + + public PodInfoRecentPulseLog readPulseLog() { + PodInfoResponse response = delegate.getPodInfo(PodInfoType.RECENT_PULSE_LOG); + return response.getPodInfo(); + } + + public OmnipodCommunicationManager getCommunicationService() { + return delegate.getCommunicationService(); + } + + public DateTime getTime() { + return delegate.getTime(); + } + + public boolean isInitialized() { + return delegate.isReadyForDelivery(); + } + + public String getPodStateAsString() { + return delegate.getPodStateAsString(); + } + + private void reportImplicitlyCanceledTbr() { + //TreatmentsPlugin plugin = TreatmentsPlugin.getPlugin(); + TreatmentsInterface plugin = activePlugin.getActiveTreatments(); + if (plugin.isTempBasalInProgress()) { + aapsLogger.debug(LTag.PUMP, "Reporting implicitly cancelled TBR to Treatments plugin"); + + long time = System.currentTimeMillis() - 1000; + + addSuccessToHistory(time, PodHistoryEntryType.CancelTemporaryBasal, null); + + TemporaryBasal temporaryBasal = new TemporaryBasal() // + .date(time) // + .duration(0) // + .pumpId(pumpStatus.tempBasalPumpId) + .source(Source.PUMP); + + plugin.addToHistoryTempBasal(temporaryBasal); + } + } + + + public long addSuccessToHistory(long requestTime, PodHistoryEntryType entryType, Object data) { + return addToHistory(requestTime, entryType, data, true); + } + + public long addFailureToHistory(long requestTime, PodHistoryEntryType entryType, Object data) { + return addToHistory(requestTime, entryType, data, false); + } + + + public long addToHistory(long requestTime, PodHistoryEntryType entryType, Object data, boolean success) { + + PodHistory podHistory = new PodHistory(requestTime, entryType); + + if (data != null) { + if (data instanceof String) { + podHistory.setData((String) data); + } else { + podHistory.setData(omnipodUtil.getGsonInstance().toJson(data)); + } + } + + podHistory.setSuccess(success); + podHistory.setPodSerial(pumpStatus.podNumber); + + MainApp.getDbHelper().createOrUpdate(podHistory); + + return podHistory.getPumpId(); + + } + + private void handleSetupActionResult(PodInitActionType podInitActionType, PodInitReceiver podInitReceiver, SetupActionResult res, long time, Profile profile) { + String comment = null; + switch (res.getResultType()) { + case FAILURE: { + aapsLogger.error(LTag.PUMP, "Setup action failed: illegal setup progress: {}", res.getSetupProgress()); + comment = getStringResource(R.string.omnipod_driver_error_invalid_progress_state, res.getSetupProgress()); + } + break; + case VERIFICATION_FAILURE: { + aapsLogger.error(LTag.PUMP, "Setup action verification failed: caught exception", res.getException()); + comment = getStringResource(R.string.omnipod_driver_error_setup_action_verification_failed); + } + break; + } + + if (podInitActionType == PodInitActionType.PairAndPrimeWizardStep) { + addToHistory(time, PodHistoryEntryType.PairAndPrime, comment, res.getResultType().isSuccess()); + } else { + addToHistory(time, PodHistoryEntryType.FillCannulaSetBasalProfile, res.getResultType().isSuccess() ? profile.getBasalValues() : comment, res.getResultType().isSuccess()); + } + + podInitReceiver.returnInitTaskStatus(podInitActionType, res.getResultType().isSuccess(), comment); + } + + private String handleAndTranslateException(Exception ex) { + String comment; + + if (ex instanceof OmnipodException) { + if (ex instanceof ActionInitializationException || ex instanceof CommandInitializationException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_parameters); + } else if (ex instanceof CommunicationException) { + comment = getStringResource(R.string.omnipod_driver_error_communication_failed); + } else if (ex instanceof CrcMismatchException) { + comment = getStringResource(R.string.omnipod_driver_error_crc_mismatch); + } else if (ex instanceof IllegalPacketTypeException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_packet_type); + } else if (ex instanceof IllegalPodProgressException || ex instanceof IllegalSetupProgressException + || ex instanceof IllegalDeliveryStatusException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_progress_state); + } else if (ex instanceof IllegalResponseException) { + comment = getStringResource(R.string.omnipod_driver_error_invalid_response); + } else if (ex instanceof MessageDecodingException) { + comment = getStringResource(R.string.omnipod_driver_error_message_decoding_failed); + } else if (ex instanceof NonceOutOfSyncException) { + comment = getStringResource(R.string.omnipod_driver_error_nonce_out_of_sync); + } else if (ex instanceof NonceResyncException) { + comment = getStringResource(R.string.omnipod_driver_error_nonce_resync_failed); + } else if (ex instanceof NotEnoughDataException) { + comment = getStringResource(R.string.omnipod_driver_error_not_enough_data); + } else if (ex instanceof PodFaultException) { + FaultEventType faultEventType = ((PodFaultException) ex).getFaultEvent().getFaultEventType(); + showPodFaultErrorDialog(faultEventType, R.raw.urgentalarm); + comment = createPodFaultErrorMessage(faultEventType); + } else if (ex instanceof PodReturnedErrorResponseException) { + comment = getStringResource(R.string.omnipod_driver_error_pod_returned_error_response); + } else { + // Shouldn't be reachable + comment = getStringResource(R.string.omnipod_driver_error_unexpected_exception_type, ex.getClass().getName()); + } + aapsLogger.error(LTag.PUMP, String.format("Caught OmnipodException[certainFailure=%s] from OmnipodManager (user-friendly error message: %s)", ((OmnipodException) ex).isCertainFailure(), comment), ex); + } else { + comment = getStringResource(R.string.omnipod_driver_error_unexpected_exception_type, ex.getClass().getName()); + aapsLogger.error(LTag.PUMP, String.format("Caught unexpected exception type[certainFailure=false] from OmnipodManager (user-friendly error message: %s)", comment), ex); + } + + return comment; + } + + private String createPodFaultErrorMessage(FaultEventType faultEventType) { + String comment; + comment = getStringResource(R.string.omnipod_driver_error_pod_fault, + ByteUtil.convertUnsignedByteToInt(faultEventType.getValue()), faultEventType.name()); + return comment; + } + + private void sendEvent(Event event) { + rxBus.send(event); + } + + private void showPodFaultErrorDialog(FaultEventType faultEventType, Integer sound) { + showErrorDialog(createPodFaultErrorMessage(faultEventType), sound); + } + + private void showErrorDialog(String message, Integer sound) { + Intent intent = new Intent(MainApp.instance(), ErrorHelperActivity.class); + intent.putExtra("soundid", sound == null ? 0 : sound); + intent.putExtra("status", message); + intent.putExtra("title", MainApp.gs(R.string.treatmentdeliveryerror)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + MainApp.instance().startActivity(intent); + } + + private void showNotification(String message, int urgency, Integer sound) { + Notification notification = new Notification( // + Notification.OMNIPOD_PUMP_ALARM, // + message, // + urgency); + if (sound != null) { + notification.soundId = sound; + } + sendEvent(new EventNewNotification(notification)); + } + + private String translateAlertType(AlertType alertType) { + if (alertType == null) { + return getStringResource(R.string.omnipod_alert_unknown_alert); + } + switch (alertType) { + case FINISH_PAIRING_REMINDER: + return getStringResource(R.string.omnipod_alert_finish_pairing_reminder); + case FINISH_SETUP_REMINDER: + return getStringResource(R.string.omnipod_alert_finish_setup_reminder_reminder); + case EXPIRATION_ALERT: + return getStringResource(R.string.omnipod_alert_expiration); + case EXPIRATION_ADVISORY_ALERT: + return getStringResource(R.string.omnipod_alert_expiration_advisory); + case SHUTDOWN_IMMINENT_ALARM: + return getStringResource(R.string.omnipod_alert_shutdown_imminent); + case LOW_RESERVOIR_ALERT: + return getStringResource(R.string.omnipod_alert_low_reservoir); + default: + return alertType.name(); + } + } + + private boolean isBolusBeepsEnabled() { + return this.pumpStatus.beepBolusEnabled; + } + + private boolean isSmbBeepsEnabled() { + return this.pumpStatus.beepSMBEnabled; + } + + private boolean isBasalBeepsEnabled() { + return this.pumpStatus.beepBasalEnabled; + } + + private boolean isTempBasalBeepsEnabled() { + return this.pumpStatus.beepTBREnabled; + } + + private String getStringResource(int id, Object... args) { + return resourceHelper.gs(id, args); + } + + static BasalSchedule mapProfileToBasalSchedule(Profile profile) { + if (profile == null) { + throw new IllegalArgumentException("Profile can not be null"); + } + Profile.ProfileValue[] basalValues = profile.getBasalValues(); + if (basalValues == null) { + throw new IllegalArgumentException("Basal values can not be null"); + } + List entries = new ArrayList<>(); + for (Profile.ProfileValue basalValue : basalValues) { + entries.add(new BasalScheduleEntry(PumpType.Insulet_Omnipod.determineCorrectBasalSize(basalValue.value), + Duration.standardSeconds(basalValue.timeAsSeconds))); + } + + return new BasalSchedule(entries); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistory.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistory.java new file mode 100644 index 0000000000..e064fb5c24 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistory.java @@ -0,0 +1,136 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.db; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + +import info.nightscout.androidaps.db.DatabaseHelper; +import info.nightscout.androidaps.db.DbObjectBase; +import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil; + +/** + * Created by andy on 30.11.2019. + */ +@DatabaseTable(tableName = DatabaseHelper.DATABASE_POD_HISTORY) +public class PodHistory implements DbObjectBase, Comparable { + + @DatabaseField(id = true) + public long date; + + private PodHistoryEntryType podHistoryEntryType; + + @DatabaseField + private long podEntryTypeCode; + + @DatabaseField + private String data; + + @DatabaseField + private boolean success; + + @DatabaseField + private long pumpId; + + @DatabaseField + private String podSerial; + + @DatabaseField + private Boolean successConfirmed; + + public PodHistory() { + generatePumpId(); + } + + + public PodHistory(PodHistoryEntryType podDbEntryType) { + this.date = System.currentTimeMillis(); + this.podHistoryEntryType = podDbEntryType; + this.podEntryTypeCode = podDbEntryType.getCode(); + generatePumpId(); + } + + + public PodHistory(long dateTimeInMillis, PodHistoryEntryType podDbEntryType) { + this.date = dateTimeInMillis; + this.podHistoryEntryType = podDbEntryType; + this.podEntryTypeCode = podDbEntryType.getCode(); + generatePumpId(); + } + + + @Override + public long getDate() { + return this.date; + } + + public void setDate(long date) { + this.date = date; + } + + public String getDateTimeString() { + return DateTimeUtil.toStringFromTimeInMillis(this.date); + } + + public PodHistoryEntryType getPodDbEntryType() { + return PodHistoryEntryType.getByCode((int) this.podEntryTypeCode); + } + + public void setPodDbEntryType(PodHistoryEntryType podDbEntryType) { + //this.podHistoryEntryType = podDbEntryType; + this.podEntryTypeCode = podDbEntryType.getCode(); + } + + public long getPodEntryTypeCode() { + return podEntryTypeCode; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public void setPumpId(long pumpId) { + this.pumpId = pumpId; + } + + public Boolean getSuccessConfirmed() { + return successConfirmed; + } + + public void setSuccessConfirmed(Boolean successConfirmed) { + this.successConfirmed = successConfirmed; + } + + @Override + public long getPumpId() { + return pumpId; + } + + private void generatePumpId() { + this.pumpId = (DateTimeUtil.toATechDate(this.date) * 100L) + podEntryTypeCode; + } + + + public String getPodSerial() { + return podSerial; + } + + public void setPodSerial(String podSerial) { + this.podSerial = podSerial; + } + + @Override + public int compareTo(PodHistory otherOne) { + return (int) (otherOne.date - this.date); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistoryEntryType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistoryEntryType.java new file mode 100644 index 0000000000..e776de681f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/db/PodHistoryEntryType.java @@ -0,0 +1,93 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.db; + +import androidx.annotation.IdRes; +import androidx.annotation.StringRes; + +import java.util.HashMap; +import java.util.Map; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpHistoryEntryGroup; + +/** + * Created by andy on 24.11.2019 + */ +public enum PodHistoryEntryType { + + PairAndPrime(1, R.string.omnipod_init_pod_wizard_step2_title, PumpHistoryEntryGroup.Prime), + FillCannulaSetBasalProfile(2, R.string.omnipod_init_pod_wizard_step4_title, PumpHistoryEntryGroup.Prime), + DeactivatePod(3, R.string.omnipod_cmd_deactivate_pod, PumpHistoryEntryGroup.Prime), + ResetPodState(4, R.string.omnipod_cmd_reset_pod, PumpHistoryEntryGroup.Prime), + + SetTemporaryBasal(10, R.string.omnipod_cmd_set_tbr, PumpHistoryEntryGroup.Basal), + CancelTemporaryBasal(11, R.string.omnipod_cmd_cancel_tbr, PumpHistoryEntryGroup.Basal), + CancelTemporaryBasalForce(12, R.string.omnipod_cmd_cancel_tbr_forced, PumpHistoryEntryGroup.Basal), + + SetBasalSchedule(20, R.string.omnipod_cmd_set_basal_schedule, PumpHistoryEntryGroup.Basal), + + GetPodStatus(30, R.string.omnipod_cmd_get_pod_status, PumpHistoryEntryGroup.Configuration), + GetPodInfo(31, R.string.omnipod_cmd_get_pod_info, PumpHistoryEntryGroup.Configuration), + SetTime(32, R.string.omnipod_cmd_set_time, PumpHistoryEntryGroup.Configuration), + + SetBolus(40, R.string.omnipod_cmd_set_bolus, PumpHistoryEntryGroup.Bolus), + CancelBolus(41, R.string.omnipod_cmd_cancel_bolus, PumpHistoryEntryGroup.Bolus), + + ConfigureAlerts(50, R.string.omnipod_cmd_configure_alerts, PumpHistoryEntryGroup.Alarm), + AcknowledgeAlerts(51, R.string.omnipod_cmd_acknowledge_alerts, PumpHistoryEntryGroup.Alarm), + + SuspendDelivery(60, R.string.omnipod_cmd_suspend_delivery, PumpHistoryEntryGroup.Basal), + ResumeDelivery(61, R.string.omnipod_cmd_resume_delivery, PumpHistoryEntryGroup.Basal), + + UnknownEntryType(99, R.string.omnipod_cmd_unknown_entry) + ; + + private int code; + private static Map instanceMap; + + @StringRes + private int resourceId; + + private PumpHistoryEntryGroup group; + + + static { + instanceMap = new HashMap<>(); + + for (PodHistoryEntryType value : values()) { + instanceMap.put(value.code, value); + } + } + + + PodHistoryEntryType(int code, @StringRes int resourceId) { + this.code = code; + this.resourceId = resourceId; + } + + PodHistoryEntryType(int code, @StringRes int resourceId, PumpHistoryEntryGroup group) { + this.code = code; + this.resourceId = resourceId; + this.group = group; + } + + public int getCode() { + return code; + } + + public PumpHistoryEntryGroup getGroup() { + return this.group; + } + + + public static PodHistoryEntryType getByCode(int code) { + if (instanceMap.containsKey(code)) { + return instanceMap.get(code); + } else { + return UnknownEntryType; + } + } + + public int getResourceId() { + return resourceId; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIComm.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIComm.java new file mode 100644 index 0000000000..698762bb15 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIComm.java @@ -0,0 +1,83 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.ui; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + +/** + * Created by andy on 4.8.2019 + */ +public class OmnipodUIComm { + + private final HasAndroidInjector injector; + private final AAPSLogger aapsLogger; + private final OmnipodUtil omnipodUtil; + private final OmnipodCommunicationManagerInterface omnipodCommunicationManager; + private final OmnipodUIPostprocessor omnipodUIPostprocessor; + + + public OmnipodUIComm( + HasAndroidInjector injector, + AAPSLogger aapsLogger, + OmnipodUtil omnipodUtil, + OmnipodUIPostprocessor omnipodUIPostprocessor, + OmnipodCommunicationManagerInterface omnipodCommunicationManager + ) { + this.injector = injector; + this.aapsLogger = aapsLogger; + this.omnipodUtil = omnipodUtil; + this.omnipodUIPostprocessor = omnipodUIPostprocessor; + this.omnipodCommunicationManager = omnipodCommunicationManager; + } + + + public OmnipodUITask executeCommand(OmnipodCommandType commandType, Object... parameters) { + + aapsLogger.warn(LTag.PUMP, "Execute Command: " + commandType.name()); + + OmnipodUITask task = new OmnipodUITask(injector, commandType, parameters); + + omnipodUtil.setCurrentCommand(commandType); + + // new Thread(() -> { + // LOG.warn("@@@ Start Thread"); + // + // task.execute(getCommunicationManager()); + // + // LOG.warn("@@@ End Thread"); + // }); + + task.execute(this.omnipodCommunicationManager); + + // for (int i = 0; i < getMaxWaitTime(commandType); i++) { + // synchronized (task) { + // // try { + // // + // // //task.wait(1000); + // // } catch (InterruptedException e) { + // // LOG.error("executeCommand InterruptedException", e); + // // } + // + // + // SystemClock.sleep(1000); + // } + // + // if (task.isReceived()) { + // break; + // } + // } + + if (!task.isReceived()) { + aapsLogger.warn(LTag.PUMP, "Reply not received for " + commandType); + } + + task.postProcess(omnipodUIPostprocessor); + + return task; + + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIPostprocessor.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIPostprocessor.java new file mode 100644 index 0000000000..cc5878c27e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUIPostprocessor.java @@ -0,0 +1,111 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.ui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.logging.L; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodPumpPluginInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; + +/** + * Created by andy on 4.8.2019 + */ + +public class OmnipodUIPostprocessor { + + + private static final Logger LOG = LoggerFactory.getLogger(L.PUMP); + + private OmnipodPumpStatus pumpStatus; + private OmnipodPumpPluginInterface omnipodPumpPlugin; + private RxBusWrapper rxBus; + + + public OmnipodUIPostprocessor(OmnipodPumpPluginInterface plugin, OmnipodPumpStatus pumpStatus) { + this.pumpStatus = pumpStatus; + this.omnipodPumpPlugin = plugin; + this.rxBus = plugin.getRxBus(); + } + + + // this is mostly intended for command that return certain statuses (Remaining Insulin, ...), and + // where responses won't be directly used + public void postProcessData(OmnipodUITask uiTask) { + + switch (uiTask.commandType) { + + case SetBolus: { + if (uiTask.returnData != null) { + + PumpEnactResult result = uiTask.returnData; + + DetailedBolusInfo detailedBolusInfo = (DetailedBolusInfo) uiTask.getObjectFromParameters(0); + + if (result.success) { + boolean isSmb = detailedBolusInfo.isSMB; + + if (!isSmb) { + pumpStatus.lastBolusAmount = detailedBolusInfo.insulin; + pumpStatus.lastBolusTime = new Date(); + } + } + } + } + break; + + case CancelTemporaryBasal: { + pumpStatus.tempBasalStart = 0; + pumpStatus.tempBasalEnd = 0; + pumpStatus.tempBasalAmount = null; + pumpStatus.tempBasalLength = null; + } + break; + +// case PairAndPrimePod: { +// if (uiTask.returnData.success) { +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.PairAndPrime, false); +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.FillCanulaSetBasalProfile, true); +// } +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.DeactivatePod, true); +// } +// break; +// +// case FillCanulaAndSetBasalProfile: { +// if (uiTask.returnData.success) { +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.FillCanulaSetBasalProfile, false); +// } +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.DeactivatePod, true); +// } +// break; +// +// case DeactivatePod: +// case ResetPodStatus: { +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.PairAndPrime, true); +// omnipodPumpPlugin.setEnableCustomAction(OmnipodCustomActionType.DeactivatePod, false); +// } +// break; + + + default: + if (isLogEnabled()) + LOG.trace("Post-processing not implemented for {}.", uiTask.commandType.name()); + + } + + + } + + + private boolean isLogEnabled() { + return L.isEnabled(L.PUMP); + } + + public RxBusWrapper getRxBus() { + return this.rxBus; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUITask.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUITask.java new file mode 100644 index 0000000000..3479a299eb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/driver/ui/OmnipodUITask.java @@ -0,0 +1,248 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.driver.ui; + +import javax.inject.Inject; + +import dagger.android.HasAndroidInjector; +import info.nightscout.androidaps.data.DetailedBolusInfo; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.common.data.TempBasalPair; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommunicationManagerInterface; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitActionType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInitReceiver; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodDeviceStatusChange; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodPumpValuesChanged; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + +/** + * Created by andy on 4.8.2019 + */ + +public class OmnipodUITask { + + @Inject RxBusWrapper rxBus; + @Inject AAPSLogger aapsLogger; + @Inject OmnipodPumpStatus omnipodPumpStatus; + @Inject OmnipodUtil omnipodUtil; + + private final HasAndroidInjector injector; + + public OmnipodCommandType commandType; + public PumpEnactResult returnData; + private String errorDescription; + private Object[] parameters; + public PodResponseType responseType; + public Object returnDataObject; + + + public OmnipodUITask(HasAndroidInjector injector, OmnipodCommandType commandType) { + this.injector = injector; + this.injector.androidInjector().inject(this); + this.commandType = commandType; + } + + + public OmnipodUITask(HasAndroidInjector injector, OmnipodCommandType commandType, Object... parameters) { + this.injector = injector; + this.injector.androidInjector().inject(this); + this.commandType = commandType; + this.parameters = parameters; + } + + + public void execute(OmnipodCommunicationManagerInterface communicationManager) { + + + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ In execute. {}", commandType); + + switch (commandType) { + + case PairAndPrimePod: + returnData = communicationManager.initPod((PodInitActionType) parameters[0], (PodInitReceiver) parameters[1], null); + break; + + case FillCanulaAndSetBasalProfile: + returnData = communicationManager.initPod((PodInitActionType) parameters[0], (PodInitReceiver) parameters[1], (Profile) parameters[2]); + break; + + case DeactivatePod: + returnData = communicationManager.deactivatePod((PodInitReceiver) parameters[0]); + break; + + case ResetPodStatus: + returnData = communicationManager.resetPodStatus(); + break; + + case SetBasalProfile: + returnData = communicationManager.setBasalProfile((Profile) parameters[0]); + break; + + case SetBolus: { + DetailedBolusInfo detailedBolusInfo = (DetailedBolusInfo) parameters[0]; + + if (detailedBolusInfo != null) + returnData = communicationManager.setBolus(detailedBolusInfo); + } + break; + + case GetPodPulseLog: + // This command is very error prone, so retry a few times if it fails + // Can take some time, but that's ok since this is a very specific feature for experts + // And will not be used by normal users + for (int i = 0; 3 > i; i++) { + try { + returnDataObject = communicationManager.readPulseLog(); + responseType = PodResponseType.Acknowledgment; + break; + } catch (Exception ex) { + { + aapsLogger.warn(LTag.PUMP, "Failed to retrieve pulse log", ex); + } + returnDataObject = null; + responseType = PodResponseType.Error; + } + } + break; + + case GetPodStatus: + returnData = communicationManager.getPodStatus(); + break; + + case CancelBolus: + returnData = communicationManager.cancelBolus(); + break; + + case SetTemporaryBasal: { + TempBasalPair tbr = getTBRSettings(); + if (tbr != null) { + returnData = communicationManager.setTemporaryBasal(tbr); + } + } + break; + + case CancelTemporaryBasal: + returnData = communicationManager.cancelTemporaryBasal(); + break; + + case AcknowledgeAlerts: + returnData = communicationManager.acknowledgeAlerts(); + break; + + case SetTime: + returnData = communicationManager.setTime(); + break; + + default: { + aapsLogger.warn(LTag.PUMP, "This commandType is not supported (yet) - {}.", commandType); + responseType = PodResponseType.Error; + } + + } + + if (returnData != null) { + responseType = returnData.success ? PodResponseType.Acknowledgment : PodResponseType.Error; + } + + } + + + private TempBasalPair getTBRSettings() { + return new TempBasalPair(getDoubleFromParameters(0), // + false, // + getIntegerFromParameters(1)); + } + + + private Float getFloatFromParameters(int index) { + return (Float) parameters[index]; + } + + public Object getObjectFromParameters(int index) { + return parameters[index]; + } + + public Double getDoubleFromParameters(int index) { + return (Double) parameters[index]; + } + + public boolean getBooleanFromParameters(int index) { + return (boolean) parameters[index]; + } + + public Integer getIntegerFromParameters(int index) { + return (Integer) parameters[index]; + } + + + public T getResult() { + return (T) returnData; + } + + + public boolean isReceived() { + return (returnData != null || errorDescription != null); + } + + + public void postProcess(OmnipodUIPostprocessor postprocessor) { + + EventOmnipodDeviceStatusChange statusChange; + + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ postProcess. {}", commandType); + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ postProcess. responseType={}", responseType); + + if (responseType == PodResponseType.Data || responseType == PodResponseType.Acknowledgment) { + postprocessor.postProcessData(this); + } + + aapsLogger.debug(LTag.PUMP, "OmnipodUITask: @@@ postProcess. responseType={}", responseType); + + if (responseType == PodResponseType.Invalid) { + statusChange = new EventOmnipodDeviceStatusChange(PodDeviceState.ErrorWhenCommunicating, + "Unsupported command in OmnipodUITask"); + omnipodPumpStatus.setLastFailedCommunicationToNow(); + rxBus.send(statusChange); + } else if (responseType == PodResponseType.Error) { + statusChange = new EventOmnipodDeviceStatusChange(PodDeviceState.ErrorWhenCommunicating, + errorDescription); + omnipodPumpStatus.setLastFailedCommunicationToNow(); + rxBus.send(statusChange); + } else { + omnipodPumpStatus.setLastCommunicationToNow(); + rxBus.send(new EventOmnipodPumpValuesChanged()); + } + + omnipodUtil.setPodDeviceState(PodDeviceState.Sleeping); + } + + + public boolean hasData() { + return (responseType == PodResponseType.Data || responseType == PodResponseType.Acknowledgment); + } + + + public Object getParameter(int index) { + return parameters[index]; + } + + + public PodResponseType getResponseType() { + return this.responseType; + } + + public boolean wasCommandSuccessful() { + if (returnData == null) { + return false; + } + return returnData.success; + } + + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodAcknowledgeAlertsChanged.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodAcknowledgeAlertsChanged.kt new file mode 100644 index 0000000000..9938319858 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodAcknowledgeAlertsChanged.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event + +/** + * Created by andy on 04.06.2018. + */ +class EventOmnipodAcknowledgeAlertsChanged : Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodDeviceStatusChange.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodDeviceStatusChange.kt new file mode 100644 index 0000000000..db46550a05 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodDeviceStatusChange.kt @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkError +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState + +/** + * Created by andy on 4.8.2019 + */ +class EventOmnipodDeviceStatusChange : Event { + + var rileyLinkServiceState: RileyLinkServiceState? = null + var rileyLinkError: RileyLinkError? = null + var podSessionState: PodSessionState? = null + var errorDescription: String? = null + var podDeviceState: PodDeviceState? = null + + @JvmOverloads + constructor(rileyLinkServiceState: RileyLinkServiceState?, rileyLinkError: RileyLinkError? = null) { + this.rileyLinkServiceState = rileyLinkServiceState + this.rileyLinkError = rileyLinkError + } + + constructor(podSessionState: PodSessionState?) { + this.podSessionState = podSessionState + } + + constructor(errorDescription: String?) { + this.errorDescription = errorDescription + } + + constructor(podDeviceState: PodDeviceState?, errorDescription: String?) { + this.podDeviceState = podDeviceState + this.errorDescription = errorDescription + } + + override fun toString(): String { + return ("EventOmnipodDeviceStatusChange [" // + + "rileyLinkServiceState=" + rileyLinkServiceState + + ", rileyLinkError=" + rileyLinkError // + + ", podSessionState=" + podSessionState // + + ", podDeviceState=" + podDeviceState + "]") + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodPumpValuesChanged.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodPumpValuesChanged.kt new file mode 100644 index 0000000000..5b0f03a241 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodPumpValuesChanged.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event + +/** + * Created by andy on 04.06.2018. + */ +class EventOmnipodPumpValuesChanged : Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodRefreshButtonState.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodRefreshButtonState.kt new file mode 100644 index 0000000000..8bbbfd33e6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/events/EventOmnipodRefreshButtonState.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.events + +import info.nightscout.androidaps.events.Event + +class EventOmnipodRefreshButtonState (val newState : Boolean): Event() \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java new file mode 100644 index 0000000000..22cc9cd020 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +public abstract class OmnipodException extends RuntimeException { + private boolean certainFailure; + + public OmnipodException(String message, boolean certainFailure) { + super(message); + this.certainFailure = certainFailure; + } + + public OmnipodException(String message, Throwable cause, boolean certainFailure) { + super(message, cause); + this.certainFailure = certainFailure; + } + + public boolean isCertainFailure() { + return certainFailure; + } + + public void setCertainFailure(boolean certainFailure) { + this.certainFailure = certainFailure; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/service/RileyLinkOmnipodService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/service/RileyLinkOmnipodService.java new file mode 100644 index 0000000000..abaaaf737a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/service/RileyLinkOmnipodService.java @@ -0,0 +1,240 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.service; + +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.os.Binder; +import android.os.IBinder; + +import com.google.gson.Gson; + +import javax.inject.Inject; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.logging.LTag; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkCommunicationManager; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RFSpy; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RileyLinkBLE; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkEncodingType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkServiceState; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLinkTargetDevice; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkService; +import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.OmnipodPumpPlugin; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationManager; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.comm.AapsOmnipodManager; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; + + +/** + * Created by andy on 4.8.2019 + * RileyLinkOmnipodService is intended to stay running when the gui-app is closed. + */ +public class RileyLinkOmnipodService extends RileyLinkService { + + @Inject OmnipodPumpPlugin omnipodPumpPlugin; + @Inject OmnipodPumpStatus omnipodPumpStatus; + @Inject OmnipodUtil omnipodUtil; + + private static RileyLinkOmnipodService instance; + + private OmnipodCommunicationManager omnipodCommunicationManager; + private AapsOmnipodManager aapsOmnipodManager; + + private IBinder mBinder = new LocalBinder(); + private boolean rileyLinkAddressChanged = false; + private boolean inPreInit = true; + + + public RileyLinkOmnipodService() { + super(); + instance = this; + } + + + public static RileyLinkOmnipodService getInstance() { + return instance; + } + + + @Override + public void onConfigurationChanged(Configuration newConfig) { + aapsLogger.warn(LTag.PUMPCOMM, "onConfigurationChanged"); + super.onConfigurationChanged(newConfig); + } + + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + + @Override + public RileyLinkEncodingType getEncoding() { + return RileyLinkEncodingType.Manchester; + } + + + /** + * If you have customized RileyLinkServiceData you need to override this + */ + public void initRileyLinkServiceData() { + + rileyLinkServiceData.targetDevice = RileyLinkTargetDevice.Omnipod; + + // get most recently used RileyLink address + rileyLinkServiceData.rileylinkAddress = sp.getString(RileyLinkConst.Prefs.RileyLinkAddress, ""); + + rileyLinkBLE = new RileyLinkBLE(injector, this); // or this + rfspy = new RFSpy(injector, rileyLinkBLE); + rfspy.startReader(); + + initializeErosOmnipodManager(); + + aapsLogger.debug(LTag.PUMPCOMM, "RileyLinkOmnipodService newly constructed"); + //omnipodPumpStatus = (OmnipodPumpStatus) omnipodPumpPlugin.getPumpStatusData(); + } + + private void initializeErosOmnipodManager() { + if (AapsOmnipodManager.getInstance() == null) { + PodSessionState podState = null; + if (sp.contains(OmnipodConst.Prefs.PodState) && omnipodUtil.getPodSessionState() == null) { + try { + Gson gson = omnipodUtil.getGsonInstance(); + String storedPodState = sp.getString(OmnipodConst.Prefs.PodState, null); + aapsLogger.info(LTag.PUMPCOMM, "PodSessionState-SP: loaded from SharedPreferences: " + storedPodState); + podState = gson.fromJson(storedPodState, PodSessionState.class); + podState.injectDaggerClass(injector); + omnipodUtil.setPodSessionState(podState); + } catch (Exception ex) { + aapsLogger.error(LTag.PUMPCOMM, "Could not deserialize Pod state", ex); + } + } + OmnipodCommunicationManager omnipodCommunicationService = new OmnipodCommunicationManager(injector, rfspy); + //omnipodCommunicationService.setPumpStatus(omnipodPumpStatus); + this.omnipodCommunicationManager = omnipodCommunicationService; + + this.aapsOmnipodManager = new AapsOmnipodManager(omnipodCommunicationService, podState, omnipodPumpStatus, + omnipodUtil, aapsLogger, rxBus, sp, resourceHelper, injector, activePlugin); + } else { + aapsOmnipodManager = AapsOmnipodManager.getInstance(); + } + } + + + public void resetRileyLinkConfiguration() { + rfspy.resetRileyLinkConfiguration(); + } + + + @Override + public RileyLinkCommunicationManager getDeviceCommunicationManager() { + return omnipodCommunicationManager; + } + + @Override + public void setPumpDeviceState(PumpDeviceState pumpDeviceState) { + this.omnipodPumpStatus.setPumpDeviceState(pumpDeviceState); + } + + + public class LocalBinder extends Binder { + + public RileyLinkOmnipodService getServiceInstance() { + return RileyLinkOmnipodService.this; + } + } + + + /* private functions */ + + // PumpInterface - REMOVE + + public boolean isInitialized() { + return RileyLinkServiceState.isReady(rileyLinkServiceData.rileyLinkServiceState); + } + + + @Override + public String getDeviceSpecificBroadcastsIdentifierPrefix() { + return null; + } + + + public boolean handleDeviceSpecificBroadcasts(Intent intent) { + return false; + } + + + @Override + public void registerDeviceSpecificBroadcasts(IntentFilter intentFilter) { + } + + + public boolean verifyConfiguration() { + try { + omnipodPumpStatus.errorDescription = "-"; + + String rileyLinkAddress = sp.getString(RileyLinkConst.Prefs.RileyLinkAddress, null); + + if (rileyLinkAddress == null) { + aapsLogger.debug(LTag.PUMPCOMM, "RileyLink address invalid: null"); + omnipodPumpStatus.errorDescription = resourceHelper.gs(R.string.medtronic_error_rileylink_address_invalid); + return false; + } else { + if (!rileyLinkAddress.matches(omnipodPumpStatus.regexMac)) { + omnipodPumpStatus.errorDescription = resourceHelper.gs(R.string.medtronic_error_rileylink_address_invalid); + aapsLogger.debug(LTag.PUMPCOMM, "RileyLink address invalid: {}", rileyLinkAddress); + } else { + if (!rileyLinkAddress.equals(this.omnipodPumpStatus.rileyLinkAddress)) { + this.omnipodPumpStatus.rileyLinkAddress = rileyLinkAddress; + rileyLinkAddressChanged = true; + } + } + } + + this.omnipodPumpStatus.beepBasalEnabled = sp.getBoolean(OmnipodConst.Prefs.BeepBasalEnabled, true); + this.omnipodPumpStatus.beepBolusEnabled = sp.getBoolean(OmnipodConst.Prefs.BeepBolusEnabled, true); + this.omnipodPumpStatus.beepSMBEnabled = sp.getBoolean(OmnipodConst.Prefs.BeepSMBEnabled, true); + this.omnipodPumpStatus.beepTBREnabled = sp.getBoolean(OmnipodConst.Prefs.BeepTBREnabled, true); + this.omnipodPumpStatus.podDebuggingOptionsEnabled = sp.getBoolean(OmnipodConst.Prefs.PodDebuggingOptionsEnabled, false); + this.omnipodPumpStatus.timeChangeEventEnabled = sp.getBoolean(OmnipodConst.Prefs.TimeChangeEventEnabled, true); + + aapsLogger.debug(LTag.PUMPCOMM, "Beeps [basal={}, bolus={}, SMB={}, TBR={}]", this.omnipodPumpStatus.beepBasalEnabled, this.omnipodPumpStatus.beepBolusEnabled, this.omnipodPumpStatus.beepSMBEnabled, this.omnipodPumpStatus.beepTBREnabled); + + reconfigureService(); + + return true; + + } catch (Exception ex) { + this.omnipodPumpStatus.errorDescription = ex.getMessage(); + aapsLogger.error(LTag.PUMPCOMM, "Error on Verification: " + ex.getMessage(), ex); + return false; + } + } + + + private boolean reconfigureService() { + + if (!inPreInit) { + + if (rileyLinkAddressChanged) { + rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.Intents.RileyLinkNewAddressSet, this); + rileyLinkAddressChanged = false; + } + } + + return (!rileyLinkAddressChanged); + } + + public boolean setNotInPreInit() { + this.inPreInit = false; + + return reconfigureService(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java new file mode 100644 index 0000000000..ba5c91c0b5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java @@ -0,0 +1,74 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.util; + +public class OmniCRC { + public static final int[] crc16lookup = new int[] { + 0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011, + 0x8033, 0x0036, 0x003c, 0x8039, 0x0028, 0x802d, 0x8027, 0x0022, + 0x8063, 0x0066, 0x006c, 0x8069, 0x0078, 0x807d, 0x8077, 0x0072, + 0x0050, 0x8055, 0x805f, 0x005a, 0x804b, 0x004e, 0x0044, 0x8041, + 0x80c3, 0x00c6, 0x00cc, 0x80c9, 0x00d8, 0x80dd, 0x80d7, 0x00d2, + 0x00f0, 0x80f5, 0x80ff, 0x00fa, 0x80eb, 0x00ee, 0x00e4, 0x80e1, + 0x00a0, 0x80a5, 0x80af, 0x00aa, 0x80bb, 0x00be, 0x00b4, 0x80b1, + 0x8093, 0x0096, 0x009c, 0x8099, 0x0088, 0x808d, 0x8087, 0x0082, + 0x8183, 0x0186, 0x018c, 0x8189, 0x0198, 0x819d, 0x8197, 0x0192, + 0x01b0, 0x81b5, 0x81bf, 0x01ba, 0x81ab, 0x01ae, 0x01a4, 0x81a1, + 0x01e0, 0x81e5, 0x81ef, 0x01ea, 0x81fb, 0x01fe, 0x01f4, 0x81f1, + 0x81d3, 0x01d6, 0x01dc, 0x81d9, 0x01c8, 0x81cd, 0x81c7, 0x01c2, + 0x0140, 0x8145, 0x814f, 0x014a, 0x815b, 0x015e, 0x0154, 0x8151, + 0x8173, 0x0176, 0x017c, 0x8179, 0x0168, 0x816d, 0x8167, 0x0162, + 0x8123, 0x0126, 0x012c, 0x8129, 0x0138, 0x813d, 0x8137, 0x0132, + 0x0110, 0x8115, 0x811f, 0x011a, 0x810b, 0x010e, 0x0104, 0x8101, + 0x8303, 0x0306, 0x030c, 0x8309, 0x0318, 0x831d, 0x8317, 0x0312, + 0x0330, 0x8335, 0x833f, 0x033a, 0x832b, 0x032e, 0x0324, 0x8321, + 0x0360, 0x8365, 0x836f, 0x036a, 0x837b, 0x037e, 0x0374, 0x8371, + 0x8353, 0x0356, 0x035c, 0x8359, 0x0348, 0x834d, 0x8347, 0x0342, + 0x03c0, 0x83c5, 0x83cf, 0x03ca, 0x83db, 0x03de, 0x03d4, 0x83d1, + 0x83f3, 0x03f6, 0x03fc, 0x83f9, 0x03e8, 0x83ed, 0x83e7, 0x03e2, + 0x83a3, 0x03a6, 0x03ac, 0x83a9, 0x03b8, 0x83bd, 0x83b7, 0x03b2, + 0x0390, 0x8395, 0x839f, 0x039a, 0x838b, 0x038e, 0x0384, 0x8381, + 0x0280, 0x8285, 0x828f, 0x028a, 0x829b, 0x029e, 0x0294, 0x8291, + 0x82b3, 0x02b6, 0x02bc, 0x82b9, 0x02a8, 0x82ad, 0x82a7, 0x02a2, + 0x82e3, 0x02e6, 0x02ec, 0x82e9, 0x02f8, 0x82fd, 0x82f7, 0x02f2, + 0x02d0, 0x82d5, 0x82df, 0x02da, 0x82cb, 0x02ce, 0x02c4, 0x82c1, + 0x8243, 0x0246, 0x024c, 0x8249, 0x0258, 0x825d, 0x8257, 0x0252, + 0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261, + 0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231, + 0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202 + }; + public static final int[] crc8lookup = new int[]{ + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, + 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, + 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, + 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, + 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, + 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, + 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, + 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, + 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, + 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, + 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, + 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, + 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, + 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, + 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 + }; + + + public static int crc16(byte[] bytes) { + int crc = 0x0000; + for (byte b : bytes) { + crc = (crc >> 8) ^ crc16lookup[(crc ^ b) & 0xff]; + } + return crc; + } + + public static byte crc8(byte[] bytes) { + byte crc = 0x00; + for (byte b : bytes) { + crc = (byte) crc8lookup[(crc ^ b) & 0xff]; + } + return crc; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java new file mode 100644 index 0000000000..f7b78250c3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.util; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.R; + +/** + * Created by andy on 4.8.2019 + */ + +public class OmnipodConst { + + static final String Prefix = "AAPS.Omnipod."; + + public class Prefs { + public static final String PodState = Prefix + "pod_state"; + public static final int BeepBasalEnabled = R.string.key_omnipod_beep_basal_enabled; + public static final int BeepBolusEnabled = R.string.key_omnipod_beep_bolus_enabled; + public static final int BeepSMBEnabled = R.string.key_omnipod_beep_smb_enabled; + public static final int BeepTBREnabled = R.string.key_omnipod_beep_tbr_enabled; + public static final int PodDebuggingOptionsEnabled = R.string.key_omnipod_pod_debugging_options_enabled; + public static final int TimeChangeEventEnabled = R.string.key_omnipod_timechange_enabled; + } + + public class Statistics { + public static final String StatsPrefix = "omnipod_"; + public static final String FirstPumpStart = Prefix + "first_pump_use"; + public static final String LastGoodPumpCommunicationTime = Prefix + "lastGoodPumpCommunicationTime"; + //public static final String LastGoodPumpFrequency = Prefix + "LastGoodPumpFrequency"; + public static final String TBRsSet = StatsPrefix + "tbrs_set"; + public static final String StandardBoluses = StatsPrefix + "std_boluses_delivered"; + public static final String SMBBoluses = StatsPrefix + "smb_boluses_delivered"; + //public static final String LastPumpHistoryEntry = StatsPrefix + "pump_history_entry"; + } + + public static final double POD_PULSE_SIZE = 0.05; + public static final double POD_BOLUS_DELIVERY_RATE = 0.025; // units per second + public static final double POD_PRIMING_DELIVERY_RATE = 0.05; // units per second + public static final double POD_CANNULA_INSERTION_DELIVERY_RATE = 0.05; // units per second + public static final double MAX_RESERVOIR_READING = 50.0; + public static final double MAX_BOLUS = 30.0; + public static final double MAX_BASAL_RATE = 30.0; + public static final Duration MAX_TEMP_BASAL_DURATION = Duration.standardHours(12); + public static final int DEFAULT_ADDRESS = 0xffffffff; + + public static final Duration AVERAGE_BOLUS_COMMAND_COMMUNICATION_DURATION = Duration.millis(1500); + + public static final Duration SERVICE_DURATION = Duration.standardHours(80); + public static final Duration EXPIRATION_ADVISORY_WINDOW = Duration.standardHours(9); + public static final Duration END_OF_SERVICE_IMMINENT_WINDOW = Duration.standardHours(1); + public static final Duration NOMINAL_POD_LIFE = Duration.standardHours(72); + public static final double LOW_RESERVOIR_ALERT = 20.0; + + public static final double POD_PRIME_BOLUS_UNITS = 2.6; + public static final double POD_CANNULA_INSERTION_BOLUS_UNITS = 0.5; +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java new file mode 100644 index 0000000000..65d9428ffc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java @@ -0,0 +1,195 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.util; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializer; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.ISODateTimeFormat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.interfaces.ActivePluginProvider; +import info.nightscout.androidaps.logging.AAPSLogger; +import info.nightscout.androidaps.plugins.bus.RxBusWrapper; +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.data.RLHistoryItem; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodCommandType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.OmnipodPodType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodDeviceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodDriverState; +import info.nightscout.androidaps.plugins.pump.omnipod.driver.OmnipodPumpStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.events.EventOmnipodDeviceStatusChange; +import info.nightscout.androidaps.utils.alertDialogs.OKDialog; + +/** + * Created by andy on 4/8/19. + */ +@Singleton +public class OmnipodUtil { + + private final AAPSLogger aapsLogger; + private final RxBusWrapper rxBus; + private final RileyLinkUtil rileyLinkUtil; + private final OmnipodPumpStatus omnipodPumpStatus; + private final ActivePluginProvider activePlugins; + + + private boolean lowLevelDebug = true; + private OmnipodCommandType currentCommand; + private Gson gsonInstance = createGson(); + //private static PodSessionState podSessionState; + //private static PodDeviceState podDeviceState; + private OmnipodPodType omnipodPodType; + private OmnipodDriverState driverState = OmnipodDriverState.NotInitalized; + + + @Inject + public OmnipodUtil( + AAPSLogger aapsLogger, + RxBusWrapper rxBus, + RileyLinkUtil rileyLinkUtil, + OmnipodPumpStatus omnipodPumpStatus, + ActivePluginProvider activePlugins + ) { + this.aapsLogger = aapsLogger; + this.rxBus = rxBus; + this.rileyLinkUtil = rileyLinkUtil; + this.omnipodPumpStatus = omnipodPumpStatus; + this.activePlugins = activePlugins; + } + + + public boolean isLowLevelDebug() { + return lowLevelDebug; + } + + + public void setLowLevelDebug(boolean lowLevelDebug) { + this.lowLevelDebug = lowLevelDebug; + } + + + public OmnipodCommandType getCurrentCommand() { + return currentCommand; + } + + + public void setCurrentCommand(OmnipodCommandType currentCommand) { + this.currentCommand = currentCommand; + + if (currentCommand != null) + rileyLinkUtil.getRileyLinkHistory().add(new RLHistoryItem(currentCommand)); + } + + + public static void displayNotConfiguredDialog(Context context) { + OKDialog.showConfirmation(context, MainApp.gs(R.string.combo_warning), + MainApp.gs(R.string.omnipod_error_operation_not_possible_no_configuration), (Runnable) null); + } + + + public OmnipodDriverState getDriverState() { + return driverState; + } + + + public void setDriverState(OmnipodDriverState state) { + if (driverState == state) + return; + + driverState = state; + omnipodPumpStatus.driverState = state; + + // TODO maybe remove +// if (OmnipodUtil.omnipodPumpStatus != null) { +// OmnipodUtil.omnipodPumpStatus.driverState = state; +// } +// +// if (OmnipodUtil.omnipodPumpPlugin != null) { +// OmnipodUtil.omnipodPumpPlugin.setDriverState(state); +// } + } + + + private Gson createGson() { + GsonBuilder gsonBuilder = new GsonBuilder() + .registerTypeAdapter(DateTime.class, (JsonSerializer) (dateTime, typeOfSrc, context) -> + new JsonPrimitive(ISODateTimeFormat.dateTime().print(dateTime))) + .registerTypeAdapter(DateTime.class, (JsonDeserializer) (json, typeOfT, context) -> + ISODateTimeFormat.dateTime().parseDateTime(json.getAsString())) + .registerTypeAdapter(DateTimeZone.class, (JsonSerializer) (timeZone, typeOfSrc, context) -> + new JsonPrimitive(timeZone.getID())) + .registerTypeAdapter(DateTimeZone.class, (JsonDeserializer) (json, typeOfT, context) -> + DateTimeZone.forID(json.getAsString())); + + return gsonBuilder.create(); + } + + + public void setPodSessionState(PodSessionState podSessionState) { + omnipodPumpStatus.podSessionState = podSessionState; + rxBus.send(new EventOmnipodDeviceStatusChange(podSessionState)); + } + + + public void setPodDeviceState(PodDeviceState podDeviceState) { + omnipodPumpStatus.podDeviceState = podDeviceState; + } + + + public void setOmnipodPodType(OmnipodPodType omnipodPodType) { + this.omnipodPodType = omnipodPodType; + } + + + public OmnipodPodType getOmnipodPodType() { + return this.omnipodPodType; + } + + + public PodDeviceState getPodDeviceState() { + return omnipodPumpStatus.podDeviceState; + } + + + public PodSessionState getPodSessionState() { + return omnipodPumpStatus.podSessionState; + } + + + public boolean isOmnipodEros() { + return this.activePlugins.getActivePump().model() == PumpType.Insulet_Omnipod; + } + + + public boolean isOmnipodDash() { + return this.activePlugins.getActivePump().model() == PumpType.Insulet_Omnipod_Dash; + } + + + public void setPumpType(PumpType pumpType_) { + omnipodPumpStatus.pumpType = pumpType_; + } + + + public PumpType getPumpType() { + return omnipodPumpStatus.pumpType; + } + + + public Gson getGsonInstance() { + return this.gsonInstance; + } + +} diff --git a/app/src/main/res/layout/omnipod_fragment.xml b/app/src/main/res/layout/omnipod_fragment.xml new file mode 100644 index 0000000000..2068f90557 --- /dev/null +++ b/app/src/main/res/layout/omnipod_fragment.xml @@ -0,0 +1,633 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +