diff --git a/app/build.gradle b/app/build.gradle index 13710f83b2adda6231962da4fd5883ef4fe80932..a8b8c6dadedc0cf41970a355f31ff8b7a0592d5f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,8 +51,15 @@ def getBugfenderApiKey() { String bugfenderApiKey = System.getenv("BUGFENDER_API_KEY") if(bugfenderApiKey == null) { - logger.warn("Bugfender API key not set") - bugfenderApiKey = "" + File file = new File("app/bugfender.properties") + if (file.exists()) { + Properties properties = new Properties() + properties.load(new FileInputStream(file.getAbsolutePath().toString())) + bugfenderApiKey = properties.getProperty("apiKey", "") + } else { + logger.warn("Bugfender API key not set") + bugfenderApiKey = "" + } } return "\"" + bugfenderApiKey + "\"" diff --git a/app/src/main/java/info/nightscout/android/UploaderApplication.java b/app/src/main/java/info/nightscout/android/UploaderApplication.java index 1bfc87f81ded988434835da9d0af962dce3ff079..28e99ae0d3ab9b10804f42181ffd84bc7b273c1a 100644 --- a/app/src/main/java/info/nightscout/android/UploaderApplication.java +++ b/app/src/main/java/info/nightscout/android/UploaderApplication.java @@ -8,9 +8,15 @@ import com.bugfender.sdk.Bugfender; import com.crashlytics.android.Crashlytics; import com.crashlytics.android.answers.Answers; +import info.nightscout.android.model.medtronicNg.BasalRate; +import info.nightscout.android.model.medtronicNg.BasalSchedule; +import info.nightscout.android.model.medtronicNg.ContourNextLinkInfo; +import info.nightscout.android.model.medtronicNg.PumpInfo; +import info.nightscout.android.model.medtronicNg.PumpStatusEvent; import io.fabric.sdk.android.Fabric; import io.realm.Realm; import io.realm.RealmConfiguration; +import io.realm.annotations.RealmModule; import uk.co.chrisjenx.calligraphy.CalligraphyConfig; /** @@ -43,9 +49,15 @@ public class UploaderApplication extends Application { Realm.init(this); RealmConfiguration realmConfiguration = new RealmConfiguration.Builder() + .modules(new MainModule()) .deleteRealmIfMigrationNeeded() .build(); Realm.setDefaultConfiguration(realmConfiguration); } + + @RealmModule(classes = {BasalRate.class, BasalSchedule.class, ContourNextLinkInfo.class, PumpInfo.class, PumpStatusEvent.class}) + public class MainModule { + } + } diff --git a/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java b/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java index f0b9dd1aa68aa5965e29420cd5c23df5870f20a9..e458cdbb43aab20544dd8ef25a17bdf4cc3f5c47 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java @@ -64,8 +64,6 @@ import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; import info.nightscout.android.R; import info.nightscout.android.USB.UsbHidDriver; @@ -73,15 +71,16 @@ import info.nightscout.android.eula.Eula; import info.nightscout.android.eula.Eula.OnEulaAgreedTo; import info.nightscout.android.medtronic.service.MedtronicCnlAlarmManager; import info.nightscout.android.medtronic.service.MedtronicCnlIntentService; -import info.nightscout.android.model.medtronicNg.PumpInfo; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; import info.nightscout.android.settings.SettingsActivity; import info.nightscout.android.utils.ConfigurationStore; import info.nightscout.android.utils.DataStore; +import info.nightscout.android.utils.StatusStore; import io.realm.Realm; -import io.realm.RealmChangeListener; +import io.realm.RealmConfiguration; import io.realm.RealmResults; import io.realm.Sort; +import io.realm.annotations.RealmModule; import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper; public class MainActivity extends AppCompatActivity implements OnSharedPreferenceChangeListener, OnEulaAgreedTo { @@ -97,14 +96,20 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc private boolean mEnableCgmService = true; private SharedPreferences prefs = null; - private PumpInfo mActivePump; private TextView mTextViewLog; // This will eventually move to a status page. + private TextView mTextViewLogButtonTop; + private TextView mTextViewLogButtonTopRecent; + private TextView mTextViewLogButtonBottom; + private TextView mTextViewLogButtonBottomRecent; + private ScrollView mScrollView; private GraphView mChart; private Handler mUiRefreshHandler = new Handler(); private Runnable mUiRefreshRunnable = new RefreshDisplayRunnable(); private Realm mRealm; + private Realm storeRealm; private StatusMessageReceiver statusMessageReceiver = new StatusMessageReceiver(); + private UpdatePumpReceiver updatePumpReceiver = new UpdatePumpReceiver(); private UsbReceiver usbReceiver = new UsbReceiver(); private BatteryReceiver batteryReceiver = new BatteryReceiver(); @@ -117,35 +122,8 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent); } - /** - * calculate the next poll timestamp based on last svg event - * - * @param pumpStatusData - * @return timestamp - */ - public static long getNextPoll(PumpStatusEvent pumpStatusData) { - long nextPoll = pumpStatusData.getSgvDate().getTime(), - now = System.currentTimeMillis(), - pollInterval = ConfigurationStore.getInstance().getPollInterval(); - - // align to next poll slot - if (nextPoll + 2 * 60 * 60 * 1000 < now) { // last event more than 2h old -> could be a calibration - nextPoll = System.currentTimeMillis() + 1000; - } else { - // align to poll interval - nextPoll += (((now - nextPoll) / pollInterval)) * pollInterval - + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS; - if (pumpStatusData.getBatteryPercentage() > 25) { - // poll every 5 min - nextPoll += pollInterval; - } else { - // if pump battery seems to be empty reduce polling to save battery (every 15 min) - //TODO add message & document it - nextPoll += ConfigurationStore.getInstance().getLowBatteryPollInterval(); - } - } - - return nextPoll; + @RealmModule(classes = {StatusStore.class}) + private class StoreModule { } @Override @@ -155,10 +133,12 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc mRealm = Realm.getDefaultInstance(); - RealmResults<PumpStatusEvent> data = mRealm.where(PumpStatusEvent.class) - .findAllSorted("eventDate", Sort.DESCENDING); - if (data.size() > 0) - dataStore.setLastPumpStatus(data.first()); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder() + .name("storerealm.realm") + .modules(new StoreModule()) + .deleteRealmIfMigrationNeeded() + .build(); + storeRealm = Realm.getInstance(realmConfiguration); setContentView(R.layout.activity_main); @@ -204,7 +184,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc statusMessageReceiver, new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_STATUS_MESSAGE)); LocalBroadcastManager.getInstance(this).registerReceiver( - new UpdatePumpReceiver(), + updatePumpReceiver, new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_UPDATE_PUMP)); mEnableCgmService = Eula.show(this, prefs); @@ -228,7 +208,6 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_USB_REGISTER)); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - if (toolbar != null) { setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(false); @@ -315,6 +294,39 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc mTextViewLog = (TextView) findViewById(R.id.textview_log); mScrollView = (ScrollView) findViewById(R.id.scrollView); mScrollView.setSmoothScrollingEnabled(true); + mTextViewLogButtonTop = (TextView) findViewById(R.id.button_log_top); + mTextViewLogButtonTop.setVisibility(View.GONE); + mTextViewLogButtonTop.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + statusMessageReceiver.changeStatusViewOlder(); + } + }); + mTextViewLogButtonTopRecent = (TextView) findViewById(R.id.button_log_top_recent); + mTextViewLogButtonTopRecent.setVisibility(View.GONE); + mTextViewLogButtonTopRecent.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + statusMessageReceiver.changeStatusViewRecent(); + } + }); + + mTextViewLogButtonBottom = (TextView) findViewById(R.id.button_log_bottom); + mTextViewLogButtonBottom.setVisibility(View.GONE); + mTextViewLogButtonBottom.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + statusMessageReceiver.changeStatusViewNewer(); + } + }); + mTextViewLogButtonBottomRecent = (TextView) findViewById(R.id.button_log_bottom_recent); + mTextViewLogButtonBottomRecent.setVisibility(View.GONE); + mTextViewLogButtonBottomRecent.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + statusMessageReceiver.changeStatusViewRecent(); + } + }); mChart = (GraphView) findViewById(R.id.chart); @@ -468,7 +480,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc private void refreshDisplay() { cancelDisplayRefreshLoop(); - mUiRefreshHandler.post(mUiRefreshRunnable);; + mUiRefreshHandler.post(mUiRefreshRunnable); } private void refreshDisplay(int delay) { cancelDisplayRefreshLoop(); @@ -488,20 +500,28 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } private void startCgmServiceDelayed(long delay) { + long now = System.currentTimeMillis(); + long start = now + 1000; + if (!mRealm.isClosed()) { + RealmResults<PumpStatusEvent> results = mRealm.where(PumpStatusEvent.class) - .findAllSorted("eventDate", Sort.DESCENDING); + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (24 * 60 * 1000))) + .equalTo("validCGM", true) + .findAllSorted("cgmDate", Sort.DESCENDING); + if (results.size() > 0) { - long nextPoll = getNextPoll(results.first()); - long pollInterval = results.first().getBatteryPercentage() > 25 ? ConfigurationStore.getInstance().getPollInterval() : ConfigurationStore.getInstance().getLowBatteryPollInterval(); - if ((nextPoll - MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS - results.first().getSgvDate().getTime()) <= pollInterval) { - startCgmService(nextPoll + delay); - sendStatus("Next poll due at: " + dateFormatter.format(nextPoll + delay)); - return; - } + long timeLastCGM = results.first().getCgmDate().getTime(); + if (now - timeLastCGM < MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS + MedtronicCnlIntentService.POLL_PERIOD_MS) + start = timeLastCGM + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS + MedtronicCnlIntentService.POLL_PERIOD_MS; } } - startCgmService(System.currentTimeMillis() + (delay == 0 ? 1000 : delay)); + + if (start - now < delay) start = now + delay; + startCgmService(start); + + if (start - now > 10 * 1000) + sendStatus("Next poll due at: " + dateFormatter.format(start)); } private void startCgmService(long initialPoll) { @@ -559,24 +579,30 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc Log.i(TAG, "onResume called"); super.onResume(); // Focus status log to most recent on returning to app - mScrollView.post(new Runnable() { - public void run() { - mScrollView.fullScroll(View.FOCUS_DOWN); - } - }); + statusMessageReceiver.changeStatusViewRecent(); } @Override protected void onDestroy() { Log.i(TAG, "onDestroy called"); + statusMessageReceiver.addMessage(MedtronicCnlIntentService.ICON_INFO + "Shutting down uploader."); + statusMessageReceiver.addMessage("-----------------------------------------------------"); + super.onDestroy(); + cancelDisplayRefreshLoop(); + + LocalBroadcastManager.getInstance(this).unregisterReceiver(statusMessageReceiver); + LocalBroadcastManager.getInstance(this).unregisterReceiver(updatePumpReceiver); + LocalBroadcastManager.getInstance(this).unregisterReceiver(usbReceiver); unregisterReceiver(usbReceiver); unregisterReceiver(batteryReceiver); PreferenceManager.getDefaultSharedPreferences(getBaseContext()).unregisterOnSharedPreferenceChangeListener(this); - cancelDisplayRefreshLoop(); + if (!storeRealm.isClosed()) { + storeRealm.close(); + } if (!mRealm.isClosed()) { mRealm.close(); } @@ -635,77 +661,6 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc startActivity(manageCNLIntent); } - private PumpInfo getActivePump() { - long activePumpMac = dataStore.getActivePumpMac(); - if (activePumpMac != 0L && (mActivePump == null || !mActivePump.isValid() || mActivePump.getPumpMac() != activePumpMac)) { - if (mActivePump != null) { - // remove listener on old pump - mRealm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm sRealm) { - mActivePump.removeAllChangeListeners(); - } - }); - mActivePump = null; - } - - PumpInfo pump = mRealm - .where(PumpInfo.class) - .equalTo("pumpMac", activePumpMac) - .findFirst(); - - if (pump != null && pump.isValid()) { - - // first change listener start can miss fresh data and not update until next poll, force a refresh now - RemoveOutdatedRecords(); - refreshDisplay(1000); - - mActivePump = pump; - mActivePump.addChangeListener(new RealmChangeListener<PumpInfo>() { - long lastQueryTS = 0; - - @Override - public void onChange(PumpInfo pump) { - // prevent double updating after deleting old events below - if (pump.getLastQueryTS() == lastQueryTS || !pump.isValid()) { - return; - } - - lastQueryTS = pump.getLastQueryTS(); - - RemoveOutdatedRecords(); - refreshDisplay(1000); - - // TODO - handle isOffline in NightscoutUploadIntentService? - } - }); - } - } - - return mActivePump; - } - - private void RemoveOutdatedRecords() { - // Delete invalid or old records from Realm - // TODO - show an error message if the valid records haven't been uploaded - final RealmResults<PumpStatusEvent> results = - mRealm.where(PumpStatusEvent.class) - .equalTo("sgv", 0) - .or() - .lessThan("sgvDate", new Date(System.currentTimeMillis() - (24 * 60 * 60 * 1000))) - .findAll(); - if (results.size() > 0) { - mRealm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - // Delete all matches - Log.d(TAG, "Deleting " + results.size() + " records from realm"); - results.deleteAllFromRealm(); - } - }); - } - } - public static String strFormatSGV(double sgvValue) { ConfigurationStore configurationStore = ConfigurationStore.getInstance(); @@ -723,87 +678,166 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } } - public static String renderTrendSymbol(PumpStatusEvent.CGM_TREND trend) { - // TODO - symbols used for trend arrow may vary per device, find a more robust solution - switch (trend) { - case DOUBLE_UP: - return "\u21c8"; - case SINGLE_UP: - return "\u2191"; - case FOURTY_FIVE_UP: - return "\u2197"; - case FLAT: - return "\u2192"; - case FOURTY_FIVE_DOWN: - return "\u2198"; - case SINGLE_DOWN: - return "\u2193"; - case DOUBLE_DOWN: - return "\u21ca"; - default: - return "\u2014"; - } - } - private class StatusMessageReceiver extends BroadcastReceiver { - private class StatusMessage { - private long timestamp; - private String message; + private static final int PAGE_SIZE = 300; + private static final int FIRSTPAGE_SIZE = 100; + private static final int STALE_MS = 72 * 60 * 60 * 1000; + private int viewPosition = 0; + private int viewPositionSecondPage = 0; - StatusMessage(String message) { - this(System.currentTimeMillis(), message); - } + @Override + public void onReceive(Context context, Intent intent) { + final String message = intent.getStringExtra(MedtronicCnlIntentService.Constants.EXTENDED_DATA); + Log.i(TAG, "Message Receiver: " + message); + addMessage(message); + } - StatusMessage(long timestamp, String message) { - this.timestamp = timestamp; - this.message = message; + private void addMessage(final String message) { + if (storeRealm.isClosed()) return; + + storeRealm.executeTransaction( + new Realm.Transaction() { + @Override + public void execute(Realm realm) { + realm.createObject(StatusStore.class).StatusMessage(message); + } + }); + if (viewPositionSecondPage < FIRSTPAGE_SIZE) viewPositionSecondPage++; // older session log begins on next page + if (viewPosition > 0) viewPosition++; // move the view pointer when not on first page to keep aligned + showLog(); + + if (viewPosition == 0) { + // auto scroll status log + if ((mScrollView.getChildAt(0).getBottom() < mScrollView.getHeight()) || ((mScrollView.getChildAt(0).getBottom() - mScrollView.getScrollY() - mScrollView.getHeight()) < (mScrollView.getHeight() / 3))) { + mScrollView.post(new Runnable() { + public void run() { + mScrollView.fullScroll(View.FOCUS_DOWN); + } + }); + } } - public String toString() { - return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(timestamp) + ": " + message; + // remove stale items but not while viewing older paged entries + if (viewPosition < viewPositionSecondPage) { + final RealmResults results = storeRealm.where(StatusStore.class) + .lessThan("timestamp", System.currentTimeMillis() - STALE_MS) + .findAll(); + if (results.size() > 0) { + storeRealm.executeTransaction( + new Realm.Transaction() { + @Override + public void execute(Realm realm) { + results.deleteAllFromRealm(); + } + }); + } } } - private final Queue<StatusMessage> messages = new ArrayBlockingQueue<>(400); - - @Override - public void onReceive(Context context, Intent intent) { - String message = intent.getStringExtra(MedtronicCnlIntentService.Constants.EXTENDED_DATA); - Log.i(TAG, "Message Receiver: " + message); + private void showLog() { + RealmResults results = storeRealm.where(StatusStore.class) + .findAllSorted("timestamp", Sort.DESCENDING); - synchronized (messages) { - while (messages.size() > 398) { - messages.poll(); - } - messages.add(new StatusMessage(message)); - } + int remain = results.size() - viewPosition; + int segment = remain; + if (viewPosition == 0 && viewPositionSecondPage < PAGE_SIZE) segment = viewPositionSecondPage; + if (segment > PAGE_SIZE) segment = PAGE_SIZE; StringBuilder sb = new StringBuilder(); - for (StatusMessage msg : messages) { - if (sb.length() > 0) - sb.append("\n"); - sb.append(msg); + if (segment > 0) { + for (int index = viewPosition; index < viewPosition + segment; index++) + sb.insert(0, results.get(index) + (sb.length() > 0 ? "\n" : "")); } - mTextViewLog.setText(sb.toString(), BufferType.EDITABLE); - // auto scroll status log - if ((mScrollView.getChildAt(0).getBottom() < mScrollView.getHeight()) || ((mScrollView.getChildAt(0).getBottom() - mScrollView.getScrollY() - mScrollView.getHeight()) < (mScrollView.getHeight() / 3))) { - mScrollView.post(new Runnable() { - public void run() { - mScrollView.fullScroll(View.FOCUS_DOWN); - } - }); + if (viewPosition > 0) { + mTextViewLogButtonBottom.setVisibility(View.VISIBLE); + mTextViewLogButtonBottomRecent.setVisibility(View.VISIBLE); + } else { + mTextViewLogButtonBottom.setVisibility(View.GONE); + mTextViewLogButtonBottomRecent.setVisibility(View.GONE); + } + if (remain > segment) { + mTextViewLogButtonTop.setVisibility(View.VISIBLE); + } else { + mTextViewLogButtonTop.setVisibility(View.GONE); + } + if (viewPosition > 0 || mScrollView.getChildAt(0).getBottom() > mScrollView.getHeight() + 100) { + mTextViewLogButtonTopRecent.setVisibility(View.VISIBLE); + } else { + mTextViewLogButtonTopRecent.setVisibility(View.GONE); } } - public void clearMessages() { - synchronized (messages) { - messages.clear(); + private void clearMessages() { + final RealmResults results = storeRealm.where(StatusStore.class) + .findAll(); + if (results.size() > 0) { + storeRealm.executeTransaction( + new Realm.Transaction() { + @Override + public void execute(Realm realm) { + results.deleteAllFromRealm(); + } + }); } - mTextViewLog.setText("", BufferType.EDITABLE); + mTextViewLogButtonTop.setVisibility(View.GONE); + mTextViewLogButtonTopRecent.setVisibility(View.GONE); + mTextViewLogButtonBottom.setVisibility(View.GONE); + mTextViewLogButtonBottomRecent.setVisibility(View.GONE); + viewPosition = 0; + viewPositionSecondPage = 0; + } + + private void changeStatusViewOlder() { + if (viewPosition == 0 && viewPositionSecondPage < PAGE_SIZE) { + viewPosition = viewPositionSecondPage; + } else { + viewPosition += PAGE_SIZE; + } + showLog(); + mScrollView.post(new Runnable() { + public void run() { + mScrollView.fullScroll(View.FOCUS_DOWN); + if (viewPosition > 0 || mScrollView.getChildAt(0).getBottom() > mScrollView.getHeight() + 100) { + mTextViewLogButtonTopRecent.setVisibility(View.VISIBLE); + } else { + mTextViewLogButtonTopRecent.setVisibility(View.GONE); + } + } + }); } + private void changeStatusViewNewer() { + viewPosition -= PAGE_SIZE; + if (viewPosition < 0) viewPosition = 0; + showLog(); + mScrollView.post(new Runnable() { + public void run() { + mScrollView.fullScroll(View.FOCUS_UP); + if (viewPosition > 0 || mScrollView.getChildAt(0).getBottom() > mScrollView.getHeight() + 100) { + mTextViewLogButtonTopRecent.setVisibility(View.VISIBLE); + } else { + mTextViewLogButtonTopRecent.setVisibility(View.GONE); + } + } + }); + } + private void changeStatusViewRecent() { + viewPosition = 0; + showLog(); + mScrollView.post(new Runnable() { + public void run() { + mScrollView.fullScroll(View.FOCUS_DOWN); + if (viewPosition > 0 || mScrollView.getChildAt(0).getBottom() > mScrollView.getHeight() + 100) { + mTextViewLogButtonTopRecent.setVisibility(View.VISIBLE); + } else { + mTextViewLogButtonTopRecent.setVisibility(View.GONE); + } + } + }); + } + } private class RefreshDisplayRunnable implements Runnable { @@ -811,52 +845,96 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc public void run() { long nextRun = 60000L; - TextView textViewBg = (TextView) findViewById(R.id.textview_bg); - TextView textViewBgTime = (TextView) findViewById(R.id.textview_bg_time); - TextView textViewUnits = (TextView) findViewById(R.id.textview_units); - if (configurationStore.isMmolxl()) { - textViewUnits.setText(R.string.text_unit_mmolxl); - } else { - textViewUnits.setText(R.string.text_unit_mgxdl); - } - TextView textViewTrend = (TextView) findViewById(R.id.textview_trend); - TextView textViewIOB = (TextView) findViewById(R.id.textview_iob); - - // Get the most recently written CGM record for the active pump. - PumpStatusEvent pumpStatusData = null; - - if (dataStore.getLastPumpStatus().getEventDate().getTime() > 0) { - pumpStatusData = dataStore.getLastPumpStatus(); - } - - updateChart(mRealm.where(PumpStatusEvent.class) - .notEqualTo("sgv", 0) - .greaterThan("sgvDate", new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24)) - .findAllSorted("sgvDate", Sort.ASCENDING)); + if (!mRealm.isClosed()) { - if (pumpStatusData != null) { - String sgvString; - if (pumpStatusData.isCgmActive()) { - sgvString = MainActivity.strFormatSGV(pumpStatusData.getSgv()); + TextView textViewBg = (TextView) findViewById(R.id.textview_bg); + TextView textViewBgTime = (TextView) findViewById(R.id.textview_bg_time); + TextView textViewUnits = (TextView) findViewById(R.id.textview_units); + if (configurationStore.isMmolxl()) { + textViewUnits.setText(R.string.text_unit_mmolxl); + } else { + textViewUnits.setText(R.string.text_unit_mgxdl); + } + TextView textViewTrend = (TextView) findViewById(R.id.textview_trend); + TextView textViewIOB = (TextView) findViewById(R.id.textview_iob); + + String timeString = "never"; + String sgvString = "\u2014"; // — + String trendString = "{ion_ios_minus_empty}"; + int trendRotation = 0; + float iob = 0; + int battery = 0; + + // most recent sgv status + RealmResults<PumpStatusEvent> sgv_results = + mRealm.where(PumpStatusEvent.class) + .equalTo("validSGV", true) + .findAllSorted("cgmDate", Sort.ASCENDING); + + if (sgv_results.size() > 0) { + long sgvtime = sgv_results.last().getCgmDate().getTime(); + nextRun = 60000L - (System.currentTimeMillis() - sgvtime) % 60000L; + timeString = (DateUtils.getRelativeTimeSpanString(sgvtime)).toString(); + sgvString = MainActivity.strFormatSGV(sgv_results.last().getSgv()); if (configurationStore.isMmolxl()) { Log.d(TAG, sgvString + " mmol/L"); } else { Log.d(TAG, sgvString + " mg/dL"); } - } else { - sgvString = "\u2014"; // — + + switch (sgv_results.last().getCgmTrend()) { + case DOUBLE_UP: + trendString = "{ion_ios_arrow_thin_up}{ion_ios_arrow_thin_up}"; + break; + case SINGLE_UP: + trendString = "{ion_ios_arrow_thin_up}"; + break; + case FOURTY_FIVE_UP: + trendRotation = -45; + trendString = "{ion_ios_arrow_thin_right}"; + break; + case FLAT: + trendString = "{ion_ios_arrow_thin_right}"; + break; + case FOURTY_FIVE_DOWN: + trendRotation = 45; + trendString = "{ion_ios_arrow_thin_right}"; + break; + case SINGLE_DOWN: + trendString = "{ion_ios_arrow_thin_down}"; + break; + case DOUBLE_DOWN: + trendString = "{ion_ios_arrow_thin_down}{ion_ios_arrow_thin_down}"; + break; + default: + trendString = "{ion_ios_minus_empty}"; + break; + } + + updateChart(sgv_results.where() + .greaterThan("cgmDate", new Date(sgvtime - 1000 * 60 * 60 * 24)) + .findAllSorted("cgmDate", Sort.ASCENDING)); } - nextRun = 60000L - (System.currentTimeMillis() - pumpStatusData.getSgvDate().getTime()) % 60000L; - textViewBg.setText(sgvString); - textViewBgTime.setText(DateUtils.getRelativeTimeSpanString(pumpStatusData.getSgvDate().getTime())); + // most recent pump status + RealmResults<PumpStatusEvent> pump_results = + mRealm.where(PumpStatusEvent.class) + .findAllSorted("eventDate", Sort.ASCENDING); - textViewTrend.setText(MainActivity.renderTrendSymbol(pumpStatusData.getCgmTrend())); - textViewIOB.setText(String.format(Locale.getDefault(), "%.2f", pumpStatusData.getActiveInsulin())); + if (pump_results.size() > 0) { + iob = pump_results.last().getActiveInsulin(); + battery = pump_results.last().getBatteryPercentage(); + } + + textViewBg.setText(sgvString); + textViewBgTime.setText(timeString); + textViewIOB.setText(String.format(Locale.getDefault(), "%.2f", iob)); + textViewTrend.setText(trendString); + textViewTrend.setRotation(trendRotation); ActionMenuItemView batIcon = ((ActionMenuItemView) findViewById(R.id.status_battery)); if (batIcon != null) { - switch (pumpStatusData.getBatteryPercentage()) { + switch (battery) { case 0: batIcon.setTitle("0%"); batIcon.setIcon(getResources().getDrawable(R.drawable.battery_0)); @@ -882,7 +960,6 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc batIcon.setIcon(getResources().getDrawable(R.drawable.battery_unknown)); } } - } // Run myself again in 60 (or less) seconds; @@ -915,7 +992,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc int pos = 0; for (PumpStatusEvent pumpStatus : results) { // turn your data into Entry objects - entries[pos++] = new DataPoint(pumpStatus.getSgvDate(), (double) pumpStatus.getSgv()); + entries[pos++] = new DataPoint(pumpStatus.getCgmDate(), (double) pumpStatus.getSgv()); } if (mChart.getSeries().size() == 0) { @@ -927,12 +1004,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc // } // entries = Arrays.copyOfRange(entries, 0, j); - PointsGraphSeries sgvSerie = new PointsGraphSeries(entries); -// sgvSerie.setSize(3.6f); -// sgvSerie.setColor(Color.LTGRAY); + PointsGraphSeries sgvSeries = new PointsGraphSeries(entries); - - sgvSerie.setOnDataPointTapListener(new OnDataPointTapListener() { + sgvSeries.setOnDataPointTapListener(new OnDataPointTapListener() { DateFormat mFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM); @Override @@ -945,7 +1019,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } }); - sgvSerie.setCustomShape(new PointsGraphSeries.CustomShape() { + sgvSeries.setCustomShape(new PointsGraphSeries.CustomShape() { @Override public void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint) { double sgv = dataPoint.getY(); @@ -967,7 +1041,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc }); mChart.getViewport().setYAxisBoundsManual(false); - mChart.addSeries(sgvSerie); + mChart.addSeries(sgvSeries); } else { if (entries.length > 0) { ((PointsGraphSeries) mChart.getSeries().get(0)).resetData(entries); @@ -1002,16 +1076,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc * has to be done in MainActivity thread */ private class UpdatePumpReceiver extends BroadcastReceiver { - @Override public void onReceive(Context context, Intent intent) { - // If the MainActivity has already been destroyed (meaning the Realm instance has been closed) - // then don't worry about processing this broadcast - if (mRealm.isClosed()) { - return; - } - //init local pump listener - getActivePump(); + refreshDisplay(500); } } @@ -1026,6 +1093,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc boolean permissionGranted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); if (permissionGranted) { Log.d(TAG, "Got permission to access USB"); + sendStatus(MedtronicCnlIntentService.ICON_INFO + "Got permission to access USB."); startCgmService(); } else { Log.d(TAG, "Still no permission for USB. Waiting..."); @@ -1043,6 +1111,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc startCgmServiceDelayed(MedtronicCnlIntentService.USB_WARMUP_TIME_MS); } else { Log.d(TAG, "No permission for USB. Waiting."); + sendStatus(MedtronicCnlIntentService.ICON_INFO + "Waiting for USB permission."); waitForUsbPermission(); } } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { @@ -1053,6 +1122,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } } else if (MedtronicCnlIntentService.Constants.ACTION_NO_USB_PERMISSION.equals(action)) { Log.d(TAG, "No permission to read the USB device."); + sendStatus(MedtronicCnlIntentService.ICON_INFO + "Requesting USB permission."); requestUsbPermission(); } else if (MedtronicCnlIntentService.Constants.ACTION_USB_REGISTER.equals(action)) { openUsbRegistration(); diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusResponseMessage.java index 5778359c0995759e19d9856d5cc20a020a40f775..39f5e1413ed2d29f0a887a1c10d3dc7ca4cc4506 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusResponseMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusResponseMessage.java @@ -13,7 +13,6 @@ import info.nightscout.android.medtronic.exception.ChecksumException; import info.nightscout.android.medtronic.exception.EncryptionException; import info.nightscout.android.medtronic.exception.UnexpectedMessageException; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; -import info.nightscout.android.utils.DataStore; import info.nightscout.android.utils.HexDump; /** @@ -23,15 +22,24 @@ public class PumpStatusResponseMessage extends MedtronicSendMessageResponseMessa private static final String TAG = PumpStatusResponseMessage.class.getSimpleName(); // Data from the Medtronic Pump Status message + + private byte pumpStatus; + private byte cgmStatus; private boolean suspended; - private boolean bolusing; + private boolean bolusingNormal; + private boolean bolusingSquare; + private boolean bolusingDual; private boolean deliveringInsulin; private boolean tempBasalActive; private boolean cgmActive; + private boolean cgmCalibrating; + private boolean cgmCalibrationComplete; + private boolean cgmException; + private boolean cgmWarmUp; private byte activeBasalPattern; private float basalRate; private float tempBasalRate; - private byte tempBasalPercentage; + private short tempBasalPercentage; private short tempBasalMinutesRemaining; private float basalUnitsDeliveredToday; private short batteryPercentage; @@ -39,12 +47,24 @@ public class PumpStatusResponseMessage extends MedtronicSendMessageResponseMessa private short minutesOfInsulinRemaining; // 25h == "more than 1 day" private float activeInsulin; private int sgv; - private Date sgvDate; + private Date cgmDate; + private byte cgmExceptionType; private boolean lowSuspendActive; private PumpStatusEvent.CGM_TREND cgmTrend; - private boolean recentBolusWizard; // Whether a bolus wizard has been run recently - private int bolusWizardBGL; // in mg/dL. 0 means no recent bolus wizard reading. + private int recentBGL; // in mg/dL. 0 means no recent finger bg reading. + private short alert; + private Date alertDate; + private float bolusingDelivered; + private short bolusingMinutesRemaining; + private short bolusingReference; + private float lastBolusAmount; + private Date lastBolusDate; + private short lastBolusReference; + private byte transmitterBattery; + private byte transmitterControl; + private short calibrationDueMinutes; + private float sensorRateOfChange; protected PumpStatusResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, UnexpectedMessageException { super(pumpSession, payload); @@ -65,77 +85,119 @@ public class PumpStatusResponseMessage extends MedtronicSendMessageResponseMessa String outputString = HexDump.dumpHexString(statusBuffer.array()); Log.d(TAG, "PAYLOAD: " + outputString); } + // Status Flags - suspended = (statusBuffer.get(0x03) & 0x01) != 0x00; - bolusing = (statusBuffer.get(0x03) & 0x02) != 0x00; - deliveringInsulin = (statusBuffer.get(0x03) & 0x10) != 0x00; - tempBasalActive = (statusBuffer.get(0x03) & 0x20) != 0x00; - cgmActive = (statusBuffer.get(0x03) & 0x40) != 0x00; + pumpStatus = statusBuffer.get(0x03); + cgmStatus = statusBuffer.get(0x41); + + suspended = (pumpStatus & 0x01) != 0x00; + bolusingNormal = (pumpStatus & 0x02) != 0x00; + bolusingSquare = (pumpStatus & 0x04) != 0x00; + bolusingDual = (pumpStatus & 0x08) != 0x00; + deliveringInsulin = (pumpStatus & 0x10) != 0x00; + tempBasalActive = (pumpStatus & 0x20) != 0x00; + cgmActive = (pumpStatus & 0x40) != 0x00; + cgmCalibrating = (cgmStatus & 0x01) != 0x00; + cgmCalibrationComplete = (cgmStatus & 0x02) != 0x00; + cgmException = (cgmStatus & 0x04) != 0x00; // Active basal pattern - activeBasalPattern = statusBuffer.get(0x1a); + activeBasalPattern = statusBuffer.get(0x1A); // Normal basal rate - long rawNormalBasal = statusBuffer.getInt(0x1b); + long rawNormalBasal = statusBuffer.getInt(0x1B); basalRate = new BigDecimal(rawNormalBasal / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); // Temp basal rate - long rawTempBasal = statusBuffer.getShort(0x21) & 0x0000ffff; + long rawTempBasal = statusBuffer.getInt(0x1F) & 0x0000000000FFFFFFL; tempBasalRate = new BigDecimal(rawTempBasal / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); // Temp basal percentage - tempBasalPercentage = statusBuffer.get(0x23); + tempBasalPercentage = (short) (statusBuffer.get(0x23) & 0x00FF); // Temp basal minutes remaining - tempBasalMinutesRemaining = (short) (statusBuffer.getShort(0x24) & 0x0000ffff); + tempBasalMinutesRemaining = (short) (statusBuffer.getShort(0x24) & 0x00FF); // Units of insulin delivered as basal today - // TODO - is this basal? Do we have a total Units delivered elsewhere? - basalUnitsDeliveredToday = statusBuffer.getInt(0x26); + long rawBasalUnitsDeliveredToday = statusBuffer.getInt(0x26) & 0x0000000000FFFFFFL; + basalUnitsDeliveredToday = new BigDecimal(rawBasalUnitsDeliveredToday / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); // Pump battery percentage - batteryPercentage = statusBuffer.get(0x2a); + batteryPercentage = statusBuffer.get(0x2A); // Reservoir amount - long rawReservoirAmount = statusBuffer.getInt(0x2b); + long rawReservoirAmount = statusBuffer.getInt(0x2B) & 0x0000000000FFFFFFL; reservoirAmount = new BigDecimal(rawReservoirAmount / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); // Amount of insulin left in pump (in minutes) - byte insulinHours = statusBuffer.get(0x2f); + byte insulinHours = statusBuffer.get(0x2F); byte insulinMinutes = statusBuffer.get(0x30); minutesOfInsulinRemaining = (short) ((insulinHours * 60) + insulinMinutes); // Active insulin - long rawActiveInsulin = statusBuffer.getInt(0x31); + long rawActiveInsulin = statusBuffer.getInt(0x31) & 0x0000000000FFFFFFL; activeInsulin = new BigDecimal(rawActiveInsulin / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); // CGM SGV - sgv = (statusBuffer.getShort(0x35) & 0x0000ffff); // In mg/DL. 0 means no CGM reading - long rtc; - long offset; - if ((sgv & 0x200) == 0x200) { - // Sensor error. Let's reset. FIXME - solve this more elegantly later - sgv = 0; - rtc = 0; - offset = 0; + sgv = (statusBuffer.getShort(0x35) & 0x0000FFFF); // In mg/DL. 0x0000 = no CGM reading, 0x03NN = sensor exception + cgmDate = MessageUtils.decodeDateTime((long) statusBuffer.getInt(0x37) & 0x00000000FFFFFFFFL, (long) statusBuffer.getInt(0x3B)); + Log.d(TAG, "original cgm/sgv date: " + cgmDate); + + if (cgmException) { + cgmExceptionType = (byte) (sgv & 0x00FF); cgmTrend = PumpStatusEvent.CGM_TREND.NOT_SET; + if (cgmExceptionType == 0x01) cgmWarmUp = true; + sgv = 0; } else { - rtc = statusBuffer.getInt(0x37) & 0x00000000ffffffffL; - offset = statusBuffer.getInt(0x3b); - cgmTrend = PumpStatusEvent.CGM_TREND.fromMessageByte(statusBuffer.get(0x40)); + cgmExceptionType = 0; + cgmTrend = PumpStatusEvent.CGM_TREND.fromMessageByte((byte) (statusBuffer.get(0x40) & 0xF0)); // masked as low nibble can contain value when transmitter battery low + cgmWarmUp = false; } - // SGV Date - sgvDate = MessageUtils.decodeDateTime(rtc, offset); - Log.d(TAG, "original sgv date: " + sgvDate); - // Predictive low suspend // TODO - there is more status info in this byte other than just a boolean yes/no - lowSuspendActive = statusBuffer.get(0x3f) != 0; + // noted: 0x01=high 0x04=before high 0x08=before low 0x0A=low 0x80=suspend 0x92=suspend low + lowSuspendActive = statusBuffer.get(0x3F) != 0; - // Recent Bolus Wizard BGL + // Recent Bolus Wizard recentBolusWizard = statusBuffer.get(0x48) != 0; - bolusWizardBGL = statusBuffer.getShort(0x49) & 0x0000ffff; // In mg/DL + + // Recent BGL + recentBGL = statusBuffer.getShort(0x49) & 0x0000FFFF; // In mg/DL + + // Active alert + alert = statusBuffer.getShort(0x4B); + alertDate = MessageUtils.decodeDateTime((long) statusBuffer.getInt(0x4D) & 0x00000000FFFFFFFFL, (long) statusBuffer.getInt(0x51)); + + // Now bolusing + long rawBolusingDelivered = statusBuffer.getInt(0x04) & 0x0000000000FFFFFFL; + bolusingDelivered = new BigDecimal(rawBolusingDelivered / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); + bolusingMinutesRemaining = statusBuffer.getShort(0x0C); + bolusingReference = statusBuffer.getShort(0x0E); + + // Last bolus + long rawLastBolusAmount = statusBuffer.getInt(0x10) & 0x0000000000FFFFFFL; + lastBolusAmount = new BigDecimal(rawLastBolusAmount / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); + lastBolusDate = MessageUtils.decodeDateTime((long) statusBuffer.getInt(0x14) & 0x00000000FFFFFFFFL, 0); + lastBolusReference = statusBuffer.getShort(0x18); + + // Calibration + calibrationDueMinutes = statusBuffer.getShort(0x43); + + // Transmitter + transmitterControl = statusBuffer.get(0x43); + transmitterBattery = statusBuffer.get(0x45); + // Normalise transmitter battery to percentage shown on pump sensor status screen + if (transmitterBattery == 0x3F) transmitterBattery = 100; + else if (transmitterBattery == 0x2B) transmitterBattery = 73; + else if (transmitterBattery == 0x27) transmitterBattery = 47; + else if (transmitterBattery == 0x23) transmitterBattery = 20; + else if (transmitterBattery == 0x10) transmitterBattery = 0; + else transmitterBattery = 0; + + // Sensor + long rawSensorRateOfChange = statusBuffer.getShort(0x46); + sensorRateOfChange = new BigDecimal(rawSensorRateOfChange / 100f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); } /** @@ -144,13 +206,24 @@ public class PumpStatusResponseMessage extends MedtronicSendMessageResponseMessa * @param pumpRecord */ public void updatePumpRecord(PumpStatusEvent pumpRecord) { + // Status Flags + pumpRecord.setPumpStatus(pumpStatus); + pumpRecord.setCgmStatus(cgmStatus); + pumpRecord.setSuspended(suspended); - pumpRecord.setBolusing(bolusing); + pumpRecord.setBolusingNormal(bolusingNormal); + pumpRecord.setBolusingSquare(bolusingSquare); + pumpRecord.setBolusingDual(bolusingDual); pumpRecord.setDeliveringInsulin(deliveringInsulin); pumpRecord.setTempBasalActive(tempBasalActive); pumpRecord.setCgmActive(cgmActive); + pumpRecord.setCgmCalibrating(cgmCalibrating); + pumpRecord.setCgmCalibrationComplete(cgmCalibrationComplete); + pumpRecord.setCgmException(cgmException); + pumpRecord.setCgmWarmUp(cgmWarmUp); + // Active basal pattern pumpRecord.setActiveBasalPattern(activeBasalPattern); @@ -183,26 +256,42 @@ public class PumpStatusResponseMessage extends MedtronicSendMessageResponseMessa // CGM SGV data pumpRecord.setSgv(sgv); - pumpRecord.setSgvDate(new Date(sgvDate.getTime() - pumpRecord.getPumpTimeOffset())); + pumpRecord.setCgmPumpDate(cgmDate); + pumpRecord.setCgmDate(new Date(cgmDate.getTime() - pumpRecord.getPumpTimeOffset())); pumpRecord.setCgmTrend(cgmTrend); + pumpRecord.setCgmExceptionType(cgmExceptionType); // Predictive low suspend // TODO - there is more status info in this byte other than just a boolean yes/no pumpRecord.setLowSuspendActive(lowSuspendActive); - // Recent Bolus Wizard BGL - if (bolusWizardBGL > 0 - && (DataStore.getInstance().getLastPumpStatus().getSgvDate().getTime() - System.currentTimeMillis() > 15 * 60 * 1000 - || (DataStore.getInstance().getLastBolusWizardBGL() != bolusWizardBGL - && DataStore.getInstance().getLastPumpStatus().getBolusWizardBGL() != bolusWizardBGL) - ) - ) { - pumpRecord.setRecentBolusWizard(true); - pumpRecord.setBolusWizardBGL(bolusWizardBGL); // In mg/DL - } else { - pumpRecord.setRecentBolusWizard(false); - pumpRecord.setBolusWizardBGL(0); // In mg/DL - } - DataStore.getInstance().setLastBolusWizardBGL(bolusWizardBGL); + // Recent BGL + pumpRecord.setRecentBGL(recentBGL); // In mg/DL + + // Active alert + pumpRecord.setAlert(alert); + pumpRecord.setAlertPumpDate(alertDate); + pumpRecord.setAlertDate(new Date(alertDate.getTime() - pumpRecord.getPumpTimeOffset())); + + // Now bolusing + pumpRecord.setBolusingDelivered(bolusingDelivered); + pumpRecord.setBolusingMinutesRemaining(bolusingMinutesRemaining); + pumpRecord.setBolusingReference(bolusingReference); + + // Last bolus + pumpRecord.setLastBolusAmount(lastBolusAmount); + pumpRecord.setLastBolusPumpDate(lastBolusDate); + pumpRecord.setLastBolusDate(new Date(lastBolusDate.getTime() - pumpRecord.getPumpTimeOffset())); + pumpRecord.setLastBolusReference(lastBolusReference); + + // Calibration + pumpRecord.setCalibrationDueMinutes(calibrationDueMinutes); + + // Transmitter + pumpRecord.setTransmitterBattery(transmitterBattery); + pumpRecord.setTransmitterControl(transmitterControl); + + // Sensor + pumpRecord.setSensorRateOfChange(sensorRateOfChange); } -} +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java index bf3c333c40b8b990778f18afff76b49638c68651..d1a80edadf8b029d8aedefa861d07879285c2c9a 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java +++ b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlIntentService.java @@ -40,33 +40,49 @@ import info.nightscout.android.utils.DataStore; import info.nightscout.android.xdrip_plus.XDripPlusUploadReceiver; import io.realm.Realm; import io.realm.RealmResults; +import io.realm.Sort; public class MedtronicCnlIntentService extends IntentService { public final static int USB_VID = 0x1a79; public final static int USB_PID = 0x6210; public final static long USB_WARMUP_TIME_MS = 5000L; + public final static long POLL_PERIOD_MS = 300000L; public final static long LOW_BATTERY_POLL_PERIOD_MS = 900000L; // Number of additional seconds to wait after the next expected CGM poll, so that we don't interfere with CGM radio comms. public final static long POLL_GRACE_PERIOD_MS = 30000L; + // Number of seconds before the next expected CGM poll that we will allow uploader comms to start public final static long POLL_PRE_GRACE_PERIOD_MS = 45000L; - - public static final String ICON_WARN = "{ion-alert-circled} "; - public static final String ICON_BGL = "{ion-waterdrop} "; - public static final String ICON_USB = "{ion-usb} "; - public static final String ICON_INFO = "{ion-information_circled} "; - public static final String ICON_HELP = "{ion-ios-lightbulb} "; - public static final String ICON_SETTING = "{ion-android-settings} "; - public static final String ICON_HEART = "{ion-heart} "; - public static final String ICON_STAR = "{ion-ios-star} "; + // cgm n/a events to trigger anti clash poll timing + public final static int POLL_ANTI_CLASH = 3; + + // TODO - use a message type and insert icon as part of ui status message handling + public static final String ICON_WARN = "{ion_alert_circled} "; + public static final String ICON_BGL = "{ion_waterdrop} "; + public static final String ICON_USB = "{ion_usb} "; + public static final String ICON_INFO = "{ion_information_circled} "; + public static final String ICON_HELP = "{ion_ios_lightbulb} "; + public static final String ICON_SETTING = "{ion_android_settings} "; + public static final String ICON_HEART = "{ion_heart} "; + public static final String ICON_LOW = "{ion_battery_low} "; + public static final String ICON_FULL = "{ion_battery_full} "; + public static final String ICON_CGM = "{ion_ios_pulse_strong} "; + public static final String ICON_SUSPEND = "{ion_pause} "; + public static final String ICON_RESUME = "{ion_play} "; + public static final String ICON_BOLUS = "{ion_skip_forward} "; + public static final String ICON_BASAL = "{ion_skip_forward} "; + public static final String ICON_CHANGE = "{ion_android_hand} "; + public static final String ICON_BELL = "{ion_android_notifications} "; + public static final String ICON_NOTE = "{ion_clipboard} "; // show warning message after repeated errors private final static int ERROR_COMMS_AT = 4; - private final static int ERROR_CONNECT_AT = 4; - private final static int ERROR_SIGNAL_AT = 4; - private final static float ERROR_UNAVAILABLE_AT = 12; // warning at - private final static float ERROR_UNAVAILABLE_RATE = 12 / 3; // expected per hour / acceptable unavailable per hour - private final static float ERROR_UNAVAILABLE_DECAY = -1; // decay rate for each good sgv received + private final static int ERROR_CONNECT_AT = 6; + private final static int ERROR_SIGNAL_AT = 6; + private final static int ERROR_PUMPLOSTSENSOR_AT = 6; + private final static int ERROR_PUMPBATTERY_AT = 3; + private final static int ERROR_PUMPCLOCK_AT = 8; + private final static int ERROR_PUMPCLOCK_MS = 10 * 60 * 1000; private static final String TAG = MedtronicCnlIntentService.class.getSimpleName(); @@ -77,7 +93,9 @@ public class MedtronicCnlIntentService extends IntentService { private DataStore dataStore = DataStore.getInstance(); private ConfigurationStore configurationStore = ConfigurationStore.getInstance(); private DateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss", Locale.US); - + private DateFormat dateFormatterNote = new SimpleDateFormat("E HH:mm", Locale.US); + private Realm realm; + private long pumpOffset; public MedtronicCnlIntentService() { super(MedtronicCnlIntentService.class.getName()); @@ -143,57 +161,23 @@ CNL: unpaired PUMP: unpaired UPLOADER: unregistered = "Invalid message received protected void onHandleIntent(Intent intent) { Log.d(TAG, "onHandleIntent called"); try { - // TODO use of ConfigurationStore is confusinng if pollInterval uses the CS, which - // uses the POLL_PERIOD_MS, while the latter constant is also used directly. - - // Note that the variable pollInterval refers to the poll we'd like to make to the pump, - // based on settings and battery level, while POLL_PERIOD_MS is used to calculate - // when the pump is going to poll data from the transmitter again. - // Thus POLL_PERIOD_MS is important to calculate times we'd be clashing with transmitter - // to pump transmissions, which are then checked against the time the uploader would - // like to poll, which is calculated using the pollInterval variable. - // TODO find better variable names to make this distinction clearer and/or if possible - // do more method extraction refactorings to make this method easier to grasp - final long timePollStarted = System.currentTimeMillis(); + long pollInterval = configurationStore.getPollInterval(); + realm = Realm.getDefaultInstance(); - long timeLastGoodSGV = dataStore.getLastPumpStatus().getSgvDate().getTime(); - if (dataStore.getLastPumpStatus().getSgv() == 0 - || timePollStarted - timeLastGoodSGV > 24 * 60 * 60 * 1000) { - timeLastGoodSGV = 0; - } - - final long timePollExpected; - if (timeLastGoodSGV != 0) { - timePollExpected = timeLastGoodSGV + POLL_PERIOD_MS + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((timePollStarted - 1000L - (timeLastGoodSGV + POLL_GRACE_PERIOD_MS)) / POLL_PERIOD_MS)); - } else { - timePollExpected = timePollStarted; - } - - // avoid polling when too close to sensor-pump comms - if (((timePollExpected - timePollStarted) > 5000L) && ((timePollExpected - timePollStarted) < (POLL_PRE_GRACE_PERIOD_MS + POLL_GRACE_PERIOD_MS))) { - sendStatus("Please wait: Pump is expecting sensor communication. Poll due in " + ((timePollExpected - timePollStarted) / 1000L) + " seconds"); - MedtronicCnlAlarmManager.setAlarm(timePollExpected); + long due = checkPollTime(); + if (due > 0) { + sendStatus("Please wait: Pump is expecting sensor communication. Poll due in " + ((due - System.currentTimeMillis()) / 1000L) + " seconds"); + MedtronicCnlAlarmManager.setAlarm(due); return; } - final short pumpBatteryLevel = dataStore.getLastPumpStatus().getBatteryPercentage(); - long pollInterval = configurationStore.getPollInterval(); - if ((pumpBatteryLevel > 0) && (pumpBatteryLevel <= 25)) { - pollInterval = configurationStore.getLowBatteryPollInterval(); - sendStatus(ICON_WARN + "Warning: pump battery low"); - if (pollInterval != configurationStore.getPollInterval()) { - sendStatus(ICON_SETTING + "Low battery poll interval: " + (pollInterval / 60000) +" minutes"); - } - } - // TODO - throw, don't return if (!openUsbDevice()) return; MedtronicCnlReader cnlReader = new MedtronicCnlReader(mHidDevice); - Realm realm = Realm.getDefaultInstance(); realm.beginTransaction(); try { @@ -223,7 +207,7 @@ CNL: unpaired PUMP: unpaired UPLOADER: unregistered = "Invalid message received // always get LinkKey on startup to handle re-paired CNL-PUMP key changes String key = null; - if (dataStore.getCommsSuccessCount() > 0) { + if (dataStore.getCommsSuccess() > 0) { key = info.getKey(); } @@ -253,22 +237,21 @@ CNL: unpaired PUMP: unpaired UPLOADER: unregistered = "Invalid message received if (radioChannel == 0) { sendStatus(ICON_WARN + "Could not communicate with the pump. Is it nearby?"); Log.i(TAG, "Could not communicate with the pump. Is it nearby?"); - dataStore.incCommsConnectThreshold(); - pollInterval = configurationStore.getPollInterval() / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L); // reduce polling interval to half until pump is available + dataStore.incCommsConnectError(); + pollInterval = POLL_PERIOD_MS / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L); // reduce polling interval to half until pump is available } else if (cnlReader.getPumpSession().getRadioRSSIpercentage() < 5) { sendStatus(String.format(Locale.getDefault(), "Connected on channel %d RSSI: %d%%", (int) radioChannel, cnlReader.getPumpSession().getRadioRSSIpercentage())); sendStatus(ICON_WARN + "Warning: pump signal too weak. Is it nearby?"); Log.i(TAG, "Warning: pump signal too weak. Is it nearby?"); - dataStore.incCommsConnectThreshold(); - dataStore.incCommsSignalThreshold(); - pollInterval = configurationStore.getPollInterval() / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L); // reduce polling interval to half until pump is available + dataStore.incCommsConnectError(); + dataStore.incCommsSignalError(); + pollInterval = POLL_PERIOD_MS / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L); // reduce polling interval to half until pump is available } else { - dataStore.decCommsConnectThreshold(); - if (cnlReader.getPumpSession().getRadioRSSIpercentage() < 20) { - if (dataStore.getCommsSignalThreshold() < ERROR_SIGNAL_AT) dataStore.incCommsSignalThreshold(); - } else { - dataStore.decCommsSignalThreshold(); - } + dataStore.decCommsConnectError(); + if (cnlReader.getPumpSession().getRadioRSSIpercentage() < 20) + dataStore.incCommsSignalError(); + else + dataStore.decCommsSignalError(); dataStore.setActivePumpMac(pumpMAC); @@ -285,79 +268,85 @@ CNL: unpaired PUMP: unpaired UPLOADER: unregistered = "Invalid message received // TODO - this should not be necessary. We should reverse lookup the device name from PumpInfo pumpRecord.setDeviceName(deviceName); + if (radioChannel == 26) cnlReader.beginEHSMSession(); // gentle persuasion to leave channel 26 (weakest for CNL and causes Pebble to lose BT often) by using EHSM to influence pump channel change + long pumpTime = cnlReader.getPumpTime().getTime(); - long pumpOffset = pumpTime - System.currentTimeMillis(); + pumpOffset = pumpTime - System.currentTimeMillis(); Log.d(TAG, "Time offset between pump and device: " + pumpOffset + " millis."); - if (Math.abs(pumpOffset) > 10 * 60 * 1000) { - sendStatus(ICON_WARN + "Warning: Time difference between Pump and Uploader excessive." - + " Pump is over " + (Math.abs(pumpOffset) / 60000L) + " minutes " + (pumpOffset > 0 ? "ahead" : "behind") + " of time used by uploader."); - sendStatus(ICON_HELP + "The uploader phone/device should have the current time provided by network. Pump clock drifts forward and needs to be set to correct time occasionally."); - } - - // TODO - send ACTION to MainActivity to show offset between pump and uploader. pumpRecord.setPumpTimeOffset(pumpOffset); - pumpRecord.setPumpDate(new Date(pumpTime - pumpOffset)); + pumpRecord.setPumpDate(new Date(pumpTime)); + pumpRecord.setEventDate(new Date(System.currentTimeMillis())); cnlReader.updatePumpStatus(pumpRecord); - if (pumpRecord.getSgv() != 0) { - sendStatus("SGV: " + MainActivity.strFormatSGV(pumpRecord.getSgv()) - + " At: " + dateFormatter.format(pumpRecord.getSgvDate().getTime()) - + " Pump: " + (pumpOffset > 0 ? "+" : "") + (pumpOffset / 1000L) + "sec"); - // Check if pump sent old event when new expected - if (dataStore.getLastPumpStatus() != null && - dataStore.getLastPumpStatus().getSgvDate() != null && - pumpRecord.getSgvDate().getTime() - dataStore.getLastPumpStatus().getSgvDate().getTime() < 5000L && - timePollExpected - timePollStarted < 5000L) { - sendStatus(ICON_WARN + "Pump sent old SGV event"); - if (dataStore.getCommsUnavailableThreshold() < ERROR_UNAVAILABLE_AT) dataStore.addCommsUnavailableThreshold(ERROR_UNAVAILABLE_RATE / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L)); - // pump may have missed sensor transmission or be delayed in posting to status message - // in most cases the next scheduled poll will have latest sgv, occasionally it is available this period after a delay - // if user selects double poll option we try again this period or wait until next - pollInterval = POLL_PERIOD_MS / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L); - } else { - dataStore.addCommsUnavailableThreshold(ERROR_UNAVAILABLE_DECAY); - } + if (radioChannel == 26) cnlReader.endEHSMSession(); + + validatePumpRecord(pumpRecord, activePump); + activePump.getPumpHistory().add(pumpRecord); + realm.commitTransaction(); + + dataStore.incCommsSuccess(); + dataStore.clearCommsError(); + + if (pumpRecord.getBatteryPercentage() <= 25) { + dataStore.incPumpBatteryError(); + pollInterval = configurationStore.getLowBatteryPollInterval(); + } else { + dataStore.clearPumpBatteryError(); + } - dataStore.clearUnavailableSGVCount(); // reset unavailable sgv count - - // Check that the record doesn't already exist before committing - RealmResults<PumpStatusEvent> checkExistingRecords = activePump.getPumpHistory() - .where() - .equalTo("sgvDate", pumpRecord.getSgvDate()) - .equalTo("sgv", pumpRecord.getSgv()) - .findAll(); - - // There should be the 1 record we've already added in this transaction. - if (checkExistingRecords.size() == 0) { - timeLastGoodSGV = pumpRecord.getSgvDate().getTime(); - activePump.getPumpHistory().add(pumpRecord); - dataStore.setLastPumpStatus(pumpRecord); - if (pumpRecord.getBolusWizardBGL() != 0) { - sendStatus(ICON_BGL +"Recent finger BG: " + MainActivity.strFormatSGV(pumpRecord.getBolusWizardBGL())); + if (pumpRecord.isCgmActive()) { + dataStore.clearPumpCgmNA(); // poll clash detection + dataStore.clearPumpLostSensorError(); + + if (pumpRecord.isCgmWarmUp()) + sendStatus(ICON_CGM + "sensor is in warm-up phase"); + else if (pumpRecord.getCalibrationDueMinutes() == 0) + sendStatus(ICON_CGM + "sensor calibration is due now!"); + else if (pumpRecord.getSgv() == 0 && pumpRecord.isCgmCalibrating()) + sendStatus(ICON_CGM + "sensor is calibrating"); + else if (pumpRecord.getSgv() == 0) + sendStatus(ICON_CGM + "sensor error (pump graph gap)"); + else { + dataStore.incCommsSgvSuccess(); + // TODO - don't convert SGV here, convert dynamically in ui status message handler + sendStatus("SGV: " + MainActivity.strFormatSGV(pumpRecord.getSgv()) + + " At: " + dateFormatter.format(pumpRecord.getCgmDate().getTime()) + + " Pump: " + (pumpOffset > 0 ? "+" : "") + (pumpOffset / 1000L) + "sec"); + if (pumpRecord.isCgmCalibrating()) + sendStatus(ICON_CGM + "sensor is calibrating"); + if (pumpRecord.isOldSgvWhenNewExpected()) { + sendStatus(ICON_CGM + "old SGV event received"); + // pump may have missed sensor transmission or be delayed in posting to status message + // in most cases the next scheduled poll will have latest sgv, occasionally it is available this period after a delay + // if user selects double poll option we try again this period or wait until next + pollInterval = configurationStore.isReducePollOnPumpAway() ? 60000 : POLL_PERIOD_MS; } } } else { - sendStatus(ICON_WARN + "SGV: unavailable from pump"); - dataStore.incUnavailableSGVCount(); // poll clash detection - if (dataStore.getCommsUnavailableThreshold() < ERROR_UNAVAILABLE_AT) dataStore.addCommsUnavailableThreshold(ERROR_UNAVAILABLE_RATE); + dataStore.incPumpCgmNA(); // poll clash detection + if (dataStore.getCommsSgvSuccess() > 0) { + dataStore.incPumpLostSensorError(); // only count errors if cgm is being used + sendStatus(ICON_CGM + "cgm n/a (pump lost sensor)"); + } else { + sendStatus(ICON_CGM + "cgm n/a"); + } } - realm.commitTransaction(); + sendStatusNotifications(pumpRecord); + // Tell the Main Activity we have new data sendMessage(Constants.ACTION_UPDATE_PUMP); - dataStore.incCommsSuccessCount(); - dataStore.clearCommsErrorCount(); } } catch (UnexpectedMessageException e) { - dataStore.incCommsErrorCount(); + dataStore.incCommsError(); pollInterval = 60000L; // retry once during this poll period, this allows for transient radio noise Log.e(TAG, "Unexpected Message", e); sendStatus(ICON_WARN + "Communication Error: " + e.getMessage()); } catch (TimeoutException e) { - dataStore.incCommsErrorCount(); + dataStore.incCommsError(); pollInterval = 90000L; // retry once during this poll period, this allows for transient radio noise Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); sendStatus(ICON_WARN + "Timeout communicating with the Contour Next Link / Pump."); @@ -369,102 +358,496 @@ CNL: unpaired PUMP: unpaired UPLOADER: unregistered = "Invalid message received cnlReader.closeConnection(); cnlReader.endPassthroughMode(); cnlReader.endControlMode(); - } catch (NoSuchAlgorithmException e) { - } - + } catch (NoSuchAlgorithmException e) {} } } catch (IOException e) { - dataStore.incCommsErrorCount(); + dataStore.incCommsError(); Log.e(TAG, "Error connecting to Contour Next Link.", e); sendStatus(ICON_WARN + "Error connecting to Contour Next Link."); } catch (ChecksumException e) { - dataStore.incCommsErrorCount(); + dataStore.incCommsError(); Log.e(TAG, "Checksum error getting message from the Contour Next Link.", e); sendStatus(ICON_WARN + "Checksum error getting message from the Contour Next Link."); } catch (EncryptionException e) { - dataStore.incCommsErrorCount(); + dataStore.incCommsError(); Log.e(TAG, "Error decrypting messages from Contour Next Link.", e); sendStatus(ICON_WARN + "Error decrypting messages from Contour Next Link."); } catch (TimeoutException e) { - dataStore.incCommsErrorCount(); + dataStore.incCommsError(); Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); sendStatus(ICON_WARN + "Timeout communicating with the Contour Next Link."); } catch (UnexpectedMessageException e) { - dataStore.incCommsErrorCount(); + dataStore.incCommsError(); Log.e(TAG, "Could not close connection.", e); sendStatus(ICON_WARN + "Could not close connection: " + e.getMessage()); } finally { - if (!realm.isClosed()) { - if (realm.isInTransaction()) { - // If we didn't commit the transaction, we've run into an error. Let's roll it back - realm.cancelTransaction(); - } - realm.close(); + if (realm.isInTransaction()) { + // If we didn't commit the transaction, we've run into an error. Let's roll it back + realm.cancelTransaction(); } + long nextpoll = requestPollTime(timePollStarted, pollInterval); + MedtronicCnlAlarmManager.setAlarm(nextpoll); + sendStatus("Next poll due at: " + dateFormatter.format(nextpoll)); + + RemoveOutdatedRecords(); uploadPollResults(); - scheduleNextPoll(timePollStarted, timeLastGoodSGV, pollInterval); + sendStatusWarnings(); + } + + } finally { + if (!realm.isClosed()) realm.close(); + MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); + } + } + + private void sendStatusNotifications(PumpStatusEvent pumpRecord) { + if (pumpRecord.isValidBGL()) + // TODO - don't convert BGL here, convert dynamically in ui status message handler + sendStatus(ICON_BGL + "Recent finger BG: " + MainActivity.strFormatSGV(pumpRecord.getRecentBGL())); + + if (pumpRecord.isValidBolus()) { + if (pumpRecord.isValidBolusSquare()) + sendStatus(ICON_BOLUS + "Square bolus delivered: " + pumpRecord.getLastBolusAmount() + "u Started: " + dateFormatter.format(pumpRecord.getLastBolusDate()) + " Duration: " + pumpRecord.getLastBolusDuration() + " minutes"); + else if (pumpRecord.isValidBolusDual()) + sendStatus(ICON_BOLUS + "Bolus (dual normal part): " + pumpRecord.getLastBolusAmount() + "u At: " + dateFormatter.format(pumpRecord.getLastBolusDate())); + else + sendStatus(ICON_BOLUS + "Bolus: " + pumpRecord.getLastBolusAmount() + "u At: " + dateFormatter.format(pumpRecord.getLastBolusDate())); + } + + if (pumpRecord.isValidTEMPBASAL()) { + if (pumpRecord.getTempBasalMinutesRemaining() > 0 && pumpRecord.getTempBasalPercentage() > 0) + sendStatus(ICON_BASAL + "Temp basal: " + pumpRecord.getTempBasalPercentage() + "% Remaining: " + pumpRecord.getTempBasalMinutesRemaining() + " minutes"); + else if (pumpRecord.getTempBasalMinutesRemaining() > 0) + sendStatus(ICON_BASAL + "Temp basal: " + pumpRecord.getTempBasalRate() + "u Remaining: " + pumpRecord.getTempBasalMinutesRemaining() + " minutes"); + else + sendStatus(ICON_BASAL + "Temp basal: stopped before expected duration"); + } + + if (pumpRecord.isValidSUSPEND()) + sendStatus(ICON_SUSPEND + "Pump suspended insulin delivery approx: " + dateFormatterNote.format(pumpRecord.getSuspendAfterDate()) + " - " + dateFormatterNote.format(pumpRecord.getSuspendBeforeDate())); + if (pumpRecord.isValidSUSPENDOFF()) + sendStatus(ICON_RESUME + "Pump resumed insulin delivery approx: " + dateFormatterNote.format(pumpRecord.getSuspendAfterDate()) + " - " + dateFormatterNote.format(pumpRecord.getSuspendBeforeDate())); + + if (pumpRecord.isValidSAGE()) + sendStatus(ICON_CHANGE + "Sensor changed approx: " + dateFormatterNote.format(pumpRecord.getSageAfterDate()) + " - " + dateFormatterNote.format(pumpRecord.getSageBeforeDate())); + if (pumpRecord.isValidCAGE()) + sendStatus(ICON_CHANGE + "Reservoir changed approx: " + dateFormatterNote.format(pumpRecord.getCageAfterDate()) + " - " + dateFormatterNote.format(pumpRecord.getCageBeforeDate())); + if (pumpRecord.isValidBATTERY()) + sendStatus(ICON_CHANGE + "Pump battery changed approx: " + dateFormatterNote.format(pumpRecord.getBatteryAfterDate()) + " - " + dateFormatterNote.format(pumpRecord.getBatteryBeforeDate())); + + if (pumpRecord.isValidALERT()) + sendStatus(ICON_BELL + "Active alert on pump At: " + dateFormatter.format(pumpRecord.getAlertDate())); + } + + private void sendStatusWarnings() { + + if (dataStore.getPumpBatteryError() >= ERROR_PUMPBATTERY_AT) { + dataStore.clearPumpBatteryError(); + sendStatus(ICON_WARN + "Warning: pump battery low"); + if (configurationStore.getLowBatteryPollInterval() != configurationStore.getPollInterval()) + sendStatus(ICON_SETTING + "Low battery poll interval: " + (configurationStore.getLowBatteryPollInterval() / 60000) + " minutes"); + } + + if (Math.abs(pumpOffset) > ERROR_PUMPCLOCK_MS) + dataStore.incPumpClockError(); + if (dataStore.getPumpClockError() >= ERROR_PUMPCLOCK_AT) { + dataStore.clearPumpClockError(); + sendStatus(ICON_WARN + "Warning: Time difference between Pump and Uploader excessive." + + " Pump is over " + (Math.abs(pumpOffset) / 60000L) + " minutes " + (pumpOffset > 0 ? "ahead" : "behind") + " of time used by uploader."); + sendStatus(ICON_HELP + "The uploader phone/device should have the current time provided by network. Pump clock drifts forward and needs to be set to correct time occasionally."); + } + + if (dataStore.getCommsError() >= ERROR_COMMS_AT) { + sendStatus(ICON_WARN + "Warning: multiple comms/timeout errors detected."); + sendStatus(ICON_HELP + "Try: disconnecting and reconnecting the Contour Next Link to phone / restarting phone / check pairing of CNL with Pump."); + } + + if (dataStore.getPumpLostSensorError() >= ERROR_PUMPLOSTSENSOR_AT) { + dataStore.clearPumpLostSensorError(); + sendStatus(ICON_WARN + "Warning: SGV is unavailable from pump often. The pump is missing transmissions from the sensor."); + sendStatus(ICON_HELP + "Keep pump on same side of body as sensor. Avoid using body sensor locations that can block radio signal."); + } + + if (dataStore.getCommsConnectError() >= ERROR_CONNECT_AT * (configurationStore.isReducePollOnPumpAway() ? 2 : 1)) { + dataStore.clearCommsConnectError(); + sendStatus(ICON_WARN + "Warning: connecting to pump is failing often."); + sendStatus(ICON_HELP + "Keep pump nearby to uploader phone/device. The body can block radio signals between pump and uploader."); + } + + if (dataStore.getCommsSignalError() >= ERROR_SIGNAL_AT) { + dataStore.clearCommsSignalError(); + sendStatus(ICON_WARN + "Warning: RSSI radio signal from pump is generally weak and may increase errors."); + sendStatus(ICON_HELP + "Keep pump nearby to uploader phone/device. The body can block radio signals between pump and uploader."); + } + } + + private void RemoveOutdatedRecords() { + final RealmResults<PumpStatusEvent> results = + realm.where(PumpStatusEvent.class) + .lessThan("eventDate", new Date(System.currentTimeMillis() - (48 * 60 * 60 * 1000))) + .findAll(); + + if (results.size() > 0) { + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Delete all matches + Log.d(TAG, "Deleting " + results.size() + " records from realm"); + results.deleteAllFromRealm(); + } + }); + } + } + + // TODO - check on optimal use of Realm search+results as we make heavy use for validation - // TODO - Refactor warning system - if (dataStore.getCommsErrorCount() >= ERROR_COMMS_AT) { - sendStatus(ICON_WARN + "Warning: multiple comms/timeout errors detected."); - sendStatus(ICON_HELP + "Try: disconnecting and reconnecting the Contour Next Link to phone / restarting phone / check pairing of CNL with Pump."); + private void validatePumpRecord(PumpStatusEvent pumpRecord, PumpInfo activePump) { + + int index; + Date uploaderStartDate = dataStore.getUploaderStartDate(); + + // TODO - pump validation is unused but will allow for future record manipulation when adding data from pump history message (gap fill) + + // validate that this contains a new PUMP record + pumpRecord.setValidPUMP(true); + + // TODO - cgm validation - handle sensor exceptions + + // validate that this contains a new CGM record + if (pumpRecord.isCgmActive()) { + pumpRecord.setValidCGM(true); + } + + // validate that this contains a new SGV record + if (pumpRecord.isCgmActive()) { + if (pumpRecord.getPumpDate().getTime() - pumpRecord.getCgmPumpDate().getTime() > POLL_PERIOD_MS + (POLL_GRACE_PERIOD_MS / 2)) + pumpRecord.setOldSgvWhenNewExpected(true); + else if (!pumpRecord.isCgmWarmUp() && pumpRecord.getSgv() > 0) { + RealmResults<PumpStatusEvent> sgv_results = activePump.getPumpHistory() + .where() + .equalTo("cgmPumpDate", pumpRecord.getCgmPumpDate()) + .equalTo("validSGV", true) + .findAll(); + if (sgv_results.size() == 0) + pumpRecord.setValidSGV(true); + } + } + + // validate that this contains a new BGL record + if (pumpRecord.getRecentBGL() != 0) { + RealmResults<PumpStatusEvent> bgl_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (20 * 60 * 1000))) + .equalTo("recentBGL", pumpRecord.getRecentBGL()) + .findAll(); + if (bgl_results.size() == 0) { + pumpRecord.setValidBGL(true); + } + } + + // TODO - square/dual stopped and new bolus started between poll snapshots, check realm history to handle this? + + // validate that this contains a new BOLUS record + RealmResults<PumpStatusEvent> lastbolus_results = activePump.getPumpHistory() + .where() + .equalTo("lastBolusPumpDate", pumpRecord.getLastBolusPumpDate()) + .equalTo("lastBolusReference", pumpRecord.getLastBolusReference()) + .equalTo("validBolus", true) + .findAll(); + + if (lastbolus_results.size() == 0) { + + RealmResults<PumpStatusEvent> bolusing_results = activePump.getPumpHistory() + .where() + .equalTo("bolusingReference", pumpRecord.getLastBolusReference()) + .greaterThan("bolusingMinutesRemaining", 10) // if a manual normal bolus referred to here while square is being delivered it will show the remaining time for all bolusing + .findAllSorted("eventDate", Sort.ASCENDING); + + if (bolusing_results.size() > 0) { + long start = pumpRecord.getLastBolusPumpDate().getTime(); + long start_bolusing = bolusing_results.first().getPumpDate().getTime(); + + // if pump battery is changed during square/dual bolus period the last bolus time will be set to this time (pump asks user to resume/cancel bolus) + // use bolusing start time when this is detected + if (start - start_bolusing > 10 * 60) + start = start_bolusing; + + long end = pumpRecord.getPumpDate().getTime(); + long duration = start_bolusing - start + (bolusing_results.first().getBolusingMinutesRemaining() * 60000); + if (start + duration > end) // was square bolus stopped before expected duration? + duration = end - start; + + // check that this was a square bolus and not a normal bolus + if (duration > 10) { + pumpRecord.setValidBolus(true); + pumpRecord.setValidBolusSquare(true); + pumpRecord.setLastBolusDate(new Date (start)); + pumpRecord.setLastBolusDuration((short) (duration / 60000)); } - if (dataStore.getCommsUnavailableThreshold() >= ERROR_UNAVAILABLE_AT) { - dataStore.clearCommsUnavailableThreshold(); - sendStatus(ICON_WARN + "Warning: SGV unavailable from pump is happening often. The pump is missing transmissions from the sensor / in warm-up phase / environment radio noise."); - sendStatus(ICON_HELP + "Keep pump on same side of body as sensor. Avoid using body sensor locations that can block radio signal. Sensor may be old / faulty and need changing (check pump graph for gaps)."); + } + + else if (pumpRecord.getLastBolusDate().getTime() >= uploaderStartDate.getTime()) { + pumpRecord.setValidBolus(true); + if (pumpRecord.getBolusingReference() == pumpRecord.getLastBolusReference() + && pumpRecord.getBolusingMinutesRemaining() > 10) { + pumpRecord.setValidBolusDual(true); + } + } + } + + // validate that this contains a new TEMP BASAL record + // temp basal: rate / percentage can be set on pump for max duration of 24 hours / 1440 minutes + RealmResults<PumpStatusEvent> tempbasal_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (24 * 60 * 60 * 1000))) + .findAllSorted("eventDate", Sort.DESCENDING); + if (pumpRecord.getTempBasalMinutesRemaining() > 0) { + index = 0; + if (tempbasal_results.size() > 1) { + short minutes = pumpRecord.getTempBasalMinutesRemaining(); + for (index = 0; index < tempbasal_results.size(); index++) { + if (tempbasal_results.get(index).getTempBasalMinutesRemaining() < minutes || + tempbasal_results.get(index).getTempBasalPercentage() != pumpRecord.getTempBasalPercentage() || + tempbasal_results.get(index).getTempBasalRate() != pumpRecord.getTempBasalRate() || + tempbasal_results.get(index).isValidTEMPBASAL()) + break; + minutes = tempbasal_results.get(index).getTempBasalMinutesRemaining(); } - if (dataStore.getCommsConnectThreshold() >= ERROR_CONNECT_AT * (configurationStore.isReducePollOnPumpAway() ? 2 : 1)) { - dataStore.clearCommsConnectThreshold(); - sendStatus(ICON_WARN + "Warning: connecting to pump is failing often."); - sendStatus(ICON_HELP + "Keep pump nearby to uploader phone/device. The body can block radio signals between pump and uploader."); + } + if (tempbasal_results.size() > 0) + if (!tempbasal_results.get(index).isValidTEMPBASAL() || + tempbasal_results.get(index).getTempBasalPercentage() != pumpRecord.getTempBasalPercentage() || + tempbasal_results.get(index).getTempBasalRate() != pumpRecord.getTempBasalRate()) { + pumpRecord.setValidTEMPBASAL(true); + pumpRecord.setTempBasalAfterDate(tempbasal_results.get(index).getEventDate()); + pumpRecord.setTempBasalBeforeDate(pumpRecord.getEventDate()); } - if (dataStore.getCommsSignalThreshold() >= ERROR_SIGNAL_AT) { - dataStore.clearCommsSignalThreshold(); - sendStatus(ICON_WARN + "Warning: RSSI radio signal from pump is generally weak and may increase errors."); - sendStatus(ICON_HELP + "Keep pump nearby to uploader phone/device. The body can block radio signals between pump and uploader."); + } else { + // check if stopped before expected duration + if (tempbasal_results.size() > 0) + if (pumpRecord.getPumpDate().getTime() - tempbasal_results.first().getPumpDate().getTime() - (tempbasal_results.first().getTempBasalMinutesRemaining() * 60 * 1000) < -60 * 1000) { + pumpRecord.setValidTEMPBASAL(true); + pumpRecord.setTempBasalAfterDate(tempbasal_results.first().getEventDate()); + pumpRecord.setTempBasalBeforeDate(pumpRecord.getEventDate()); } + } + // validate that this contains a new SUSPEND record + RealmResults<PumpStatusEvent> suspend_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (2 * 60 * 60 * 1000))) + .equalTo("validSUSPEND", true) + .or() + .equalTo("validSUSPENDOFF", true) + .findAllSorted("eventDate", Sort.DESCENDING); + + if (suspend_results.size() > 0) { + // new valid suspend - set temp basal for 0u 60m in NS + if (pumpRecord.isSuspended() && suspend_results.first().isValidSUSPENDOFF()) { + pumpRecord.setValidSUSPEND(true); } - } finally { - MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); + // continuation valid suspend every 30m - set temp basal for 0u 60m in NS + else if (pumpRecord.isSuspended() && suspend_results.first().isValidSUSPEND() && + pumpRecord.getEventDate().getTime() - suspend_results.first().getEventDate().getTime() >= 30 * 60 * 1000) { + pumpRecord.setValidSUSPEND(true); + } + // valid suspendoff - set temp stopped in NS + else if (!pumpRecord.isSuspended() && suspend_results.first().isValidSUSPEND() && + pumpRecord.getEventDate().getTime() - suspend_results.first().getEventDate().getTime() <= 60 * 60 * 1000) { + pumpRecord.setValidSUSPENDOFF(true); + RealmResults<PumpStatusEvent> suspendended_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (2 * 60 * 60 * 1000))) + .findAllSorted("eventDate", Sort.DESCENDING); + pumpRecord.setSuspendAfterDate(suspendended_results.first().getEventDate()); + pumpRecord.setSuspendBeforeDate(pumpRecord.getEventDate()); + } + } + else if (pumpRecord.isSuspended()) { + pumpRecord.setValidSUSPEND(true); } + + // absolute suspend start time approx to after-before range + if (pumpRecord.isValidSUSPEND()) { + RealmResults<PumpStatusEvent> suspendstarted_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (12 * 60 * 60 * 1000))) + .findAllSorted("eventDate", Sort.ASCENDING); + index = suspendstarted_results.size(); + if (index > 0) { + while (index > 0) { + index--; + if (!suspendstarted_results.get(index).isSuspended()) + break; + } + pumpRecord.setSuspendAfterDate(suspendstarted_results.get(index).getEventDate()); + } + else { + pumpRecord.setSuspendAfterDate(pumpRecord.getEventDate()); + } + if (++index < suspendstarted_results.size()) + pumpRecord.setSuspendBeforeDate(suspendstarted_results.get(index).getEventDate()); + else + pumpRecord.setSuspendBeforeDate(pumpRecord.getEventDate()); + } + + // validate that this contains a new SAGE record + if (pumpRecord.isCgmWarmUp()) { + RealmResults<PumpStatusEvent> sage_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (130 * 60 * 1000))) + .equalTo("validSAGE", true) + .findAll(); + if (sage_results.size() == 0) { + pumpRecord.setValidSAGE(true); + RealmResults<PumpStatusEvent> sagedate_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (6 * 60 * 60 * 1000))) + .findAllSorted("eventDate", Sort.DESCENDING); + pumpRecord.setSageAfterDate(sagedate_results.first().getEventDate()); + pumpRecord.setSageBeforeDate(pumpRecord.getEventDate()); + } + } else if (pumpRecord.isCgmActive() && pumpRecord.getTransmitterBattery() == 100) { + // note: transmitter battery can fluctuate when on the edge of a state change + RealmResults<PumpStatusEvent> sagebattery_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (6 * 60 * 60 * 1000))) + .equalTo("cgmActive", true) + .lessThan("transmitterBattery", 50) + .findAllSorted("eventDate", Sort.DESCENDING); + if (sagebattery_results.size() > 0) { + RealmResults<PumpStatusEvent> sage_valid_results = activePump.getPumpHistory() + .where() + .greaterThanOrEqualTo("eventDate", sagebattery_results.first().getEventDate()) + .equalTo("validSAGE", true) + .findAll(); + if (sage_valid_results.size() == 0) { + pumpRecord.setValidSAGE(true); + pumpRecord.setSageAfterDate(sagebattery_results.first().getEventDate()); + pumpRecord.setSageBeforeDate(pumpRecord.getEventDate()); + } + } + } + + // validate that this contains a new CAGE record + RealmResults<PumpStatusEvent> cage_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (6 * 60 * 60 * 1000))) + .lessThan("reservoirAmount", pumpRecord.getReservoirAmount()) + .findAllSorted("eventDate", Sort.DESCENDING); + if (cage_results.size() > 0) { + RealmResults<PumpStatusEvent> cage_valid_results = activePump.getPumpHistory() + .where() + .greaterThanOrEqualTo("eventDate", cage_results.first().getEventDate()) + .equalTo("validCAGE", true) + .findAll(); + if (cage_valid_results.size() == 0) { + pumpRecord.setValidCAGE(true); + pumpRecord.setCageAfterDate(cage_results.first().getEventDate()); + pumpRecord.setCageBeforeDate(pumpRecord.getEventDate()); + } + } + + // validate that this contains a new BATTERY record + RealmResults<PumpStatusEvent> battery_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (6 * 60 * 60 * 1000))) + .lessThan("batteryPercentage", pumpRecord.getBatteryPercentage()) + .findAllSorted("eventDate", Sort.DESCENDING); + if (battery_results.size() > 0) { + RealmResults<PumpStatusEvent> battery_valid_results = activePump.getPumpHistory() + .where() + .greaterThanOrEqualTo("eventDate", battery_results.first().getEventDate()) + .equalTo("validBATTERY", true) + .findAll(); + if (battery_valid_results.size() == 0) { + pumpRecord.setValidBATTERY(true); + pumpRecord.setBatteryAfterDate(battery_results.first().getEventDate()); + pumpRecord.setBatteryBeforeDate(pumpRecord.getEventDate()); + } + } + + // validate that this contains a new ALERT record + if (pumpRecord.getAlert() > 0) { + RealmResults<PumpStatusEvent> alert_results = activePump.getPumpHistory() + .where() + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (6 * 60 * 60 * 1000))) + .findAllSorted("eventDate", Sort.DESCENDING); + if (alert_results.size() > 0) { + if (alert_results.first().getAlert() != pumpRecord.getAlert()) + pumpRecord.setValidALERT(true); + } + } + } - // TODO - Refactor polling system and make super clear how polling is calculated and why certain precautions are needed - private void scheduleNextPoll(long timePollStarted, long timeLastGoodSGV, long pollInterval) { - // smart polling and pump-sensor poll clash detection - long now = System.currentTimeMillis(); - long lastActualPollTime = timePollStarted; - if (timeLastGoodSGV > 0) { - lastActualPollTime = timeLastGoodSGV + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((now - timeLastGoodSGV + POLL_GRACE_PERIOD_MS) / POLL_PERIOD_MS)); + // pollInterval: default = POLL_PERIOD_MS (time to pump cgm reading) + // + // Can be requested at a shorter or longer interval, used to request a retry before next expected cgm data or to extend poll times due to low pump battery. + // Requests the next poll based on the actual time last cgm data was available on the pump and adding the interval + // if this time is already stale then the next actual cgm time will be used + // Any poll time request that falls within the pre-grace/grace period will be pushed to the next safe time slot + + private long requestPollTime(long lastPoll, long pollInterval) { + + int pumpCgmNA = dataStore.getPumpCgmNA(); + + RealmResults<PumpStatusEvent> cgmresults = realm.where(PumpStatusEvent.class) + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (24 * 60 * 1000))) + .equalTo("validCGM", true) + .findAllSorted("cgmDate", Sort.DESCENDING); + long timeLastCGM = 0; + if (cgmresults.size() > 0) { + timeLastCGM = cgmresults.first().getCgmDate().getTime(); } + + long now = System.currentTimeMillis(); + long lastActualPollTime = lastPoll; + if (timeLastCGM > 0) + lastActualPollTime = timeLastCGM + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((now - timeLastCGM + POLL_GRACE_PERIOD_MS) / POLL_PERIOD_MS)); long nextActualPollTime = lastActualPollTime + POLL_PERIOD_MS; long nextRequestedPollTime = lastActualPollTime + pollInterval; - // check if request is really needed - if (nextRequestedPollTime - now < 10000L) { + + // check if requested poll is stale + if (nextRequestedPollTime - now < 10 * 1000) nextRequestedPollTime = nextActualPollTime; - } - // extended unavailable SGV may be due to clash with the current polling time - // while we wait for a good SGV event, polling is auto adjusted by offsetting the next poll based on miss count - if (dataStore.getUnavailableSGVCount() > 0) { - if (timeLastGoodSGV == 0) { - nextRequestedPollTime += POLL_PERIOD_MS / 5L; // if there is a uploader/sensor poll clash on startup then this will push the next attempt out by 60 seconds - } else if (dataStore.getUnavailableSGVCount() > 2) { - sendStatus(ICON_WARN + "Warning: No SGV available from pump for " + dataStore.getUnavailableSGVCount() + " attempts"); - long offsetPollTime = ((long) ((dataStore.getUnavailableSGVCount() - 2) % 5)) * (POLL_PERIOD_MS / 10L); // adjust poll time in 1/10 steps to avoid potential poll clash (max adjustment at 5/10) - sendStatus("Adjusting poll: " + dateFormatter.format(nextRequestedPollTime) + " +" + (offsetPollTime / 1000) + "sec"); - nextRequestedPollTime += offsetPollTime; - } - } + + // extended unavailable cgm may be due to clash with the current polling time + // while we wait for a cgm event, polling is auto adjusted by offsetting the next poll based on miss count + + if (timeLastCGM == 0) + nextRequestedPollTime += 15 * 1000; // push poll time forward to avoid potential clash when no previous poll time available to sync with + else if (pumpCgmNA >= POLL_ANTI_CLASH) + nextRequestedPollTime += (((pumpCgmNA - POLL_ANTI_CLASH) % 3) + 1) * 30 * 1000; // adjust poll time in 30 second steps to avoid potential poll clash (adjustment: poll+30s / poll+60s / poll+90s) + // check if requested poll time is too close to next actual poll time if (nextRequestedPollTime > nextActualPollTime - POLL_GRACE_PERIOD_MS - POLL_PRE_GRACE_PERIOD_MS && nextRequestedPollTime < nextActualPollTime) { nextRequestedPollTime = nextActualPollTime; } - MedtronicCnlAlarmManager.setAlarm(nextRequestedPollTime); - sendStatus("Next poll due at: " + dateFormatter.format(nextRequestedPollTime)); + + return nextRequestedPollTime; + } + + private long checkPollTime() { + long due = 0; + + RealmResults<PumpStatusEvent> cgmresults = realm.where(PumpStatusEvent.class) + .greaterThan("eventDate", new Date(System.currentTimeMillis() - (24 * 60 * 1000))) + .equalTo("validCGM", true) + .findAllSorted("cgmDate", Sort.DESCENDING); + + if (cgmresults.size() > 0) { + long now = System.currentTimeMillis(); + long timeLastCGM = cgmresults.first().getCgmDate().getTime(); + long timePollExpected = timeLastCGM + POLL_PERIOD_MS + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((now - 1000L - (timeLastCGM + POLL_GRACE_PERIOD_MS)) / POLL_PERIOD_MS)); + // avoid polling when too close to sensor-pump comms + if (((timePollExpected - now) > 5000L) && ((timePollExpected - now) < (POLL_PRE_GRACE_PERIOD_MS + POLL_GRACE_PERIOD_MS))) + due = timePollExpected; + } + + return due; } /** @@ -549,4 +932,4 @@ CNL: unpaired PUMP: unpaired UPLOADER: unregistered = "Invalid message received public static final String EXTENDED_DATA = "info.nightscout.android.medtronic.service.DATA"; } -} +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java index 75847c83ce358679ce2ce14bcc0ae728675d4b13..f0d9469626756391b0e1eea41125528b65cfa8e1 100644 --- a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java +++ b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpStatusEvent.java @@ -3,7 +3,6 @@ package info.nightscout.android.model.medtronicNg; import java.util.Date; import io.realm.RealmObject; -import io.realm.annotations.Ignore; import io.realm.annotations.Index; /** @@ -11,20 +10,31 @@ import io.realm.annotations.Index; */ public class PumpStatusEvent extends RealmObject { @Index - private Date eventDate; // The actual time of the event (assume the capture device eventDate/time is accurate) - private Date pumpDate; // The eventDate/time on the pump at the time of the event + private Date eventDate; // The actual time of the this event (uploader device time) + private Date pumpDate; // The time on the pump at the time of the this event + private long pumpTimeOffset; // millis the pump is ahead + private String deviceName; // Data from the Medtronic Pump Status message + + private byte pumpStatus; + private byte cgmStatus; private boolean suspended; - private boolean bolusing; + private boolean bolusingNormal; + private boolean bolusingSquare; + private boolean bolusingDual; private boolean deliveringInsulin; private boolean tempBasalActive; private boolean cgmActive; + private boolean cgmCalibrating; + private boolean cgmCalibrationComplete; + private boolean cgmException; + private boolean cgmWarmUp; private byte activeBasalPattern; private float basalRate; private float tempBasalRate; - private byte tempBasalPercentage; + private short tempBasalPercentage; private short tempBasalMinutesRemaining; private float basalUnitsDeliveredToday; private short batteryPercentage; @@ -32,15 +42,55 @@ public class PumpStatusEvent extends RealmObject { private short minutesOfInsulinRemaining; // 25h == "more than 1 day" private float activeInsulin; private int sgv; - private Date sgvDate; + private Date cgmDate; + private Date cgmPumpDate; + private byte cgmExceptionType; private boolean lowSuspendActive; private String cgmTrend; - private boolean recentBolusWizard; // Whether a bolus wizard has been run recently - private int bolusWizardBGL; // in mg/dL. 0 means no recent bolus wizard reading. - - @Ignore - private long pumpTimeOffset; // millis the pump is ahead + private int recentBGL; // in mg/dL. 0 means no recent finger bg reading. + private short alert; + private Date alertDate; + private Date alertPumpDate; + private float bolusingDelivered; + private short bolusingMinutesRemaining; + private short bolusingReference; + private float lastBolusAmount; + private Date lastBolusDate; + private Date lastBolusPumpDate; + private short lastBolusDuration; + private short lastBolusReference; + private byte transmitterBattery; + private byte transmitterControl; + private short calibrationDueMinutes; + private float sensorRateOfChange; + + private boolean oldSgvWhenNewExpected = false; + + private boolean validPUMP = false; + private boolean validCGM = false; + private boolean validSGV = false; + private boolean validBGL = false; + private boolean validBolus = false; + private boolean validBolusDual = false; + private boolean validBolusSquare = false; + private boolean validALERT = false; + private boolean validSUSPEND = false; + private boolean validSUSPENDOFF = false; + private Date suspendAfterDate; + private Date suspendBeforeDate; + private boolean validTEMPBASAL = false; + private Date tempBasalAfterDate; + private Date tempBasalBeforeDate; + private boolean validCAGE = false; + private Date cageAfterDate; + private Date cageBeforeDate; + private boolean validSAGE = false; + private Date sageAfterDate; + private Date sageBeforeDate; + private boolean validBATTERY = false; + private Date batteryAfterDate; + private Date batteryBeforeDate; @Index private boolean uploaded = false; @@ -50,12 +100,14 @@ public class PumpStatusEvent extends RealmObject { this.eventDate = new Date(); } + public void setEventDate(Date eventDate) { + this.eventDate = eventDate; + } + public Date getEventDate() { return eventDate; } - // No EventDate setter. The eventDate is set at the time that the PumpStatusEvent is created. - public Date getPumpDate() { return pumpDate; } @@ -131,12 +183,12 @@ public class PumpStatusEvent extends RealmObject { return recentBolusWizard; } - public int getBolusWizardBGL() { - return bolusWizardBGL; + public int getRecentBGL() { + return recentBGL; } - public void setBolusWizardBGL(int bolusWizardBGL) { - this.bolusWizardBGL = bolusWizardBGL; + public void setRecentBGL(int recentBGL) { + this.recentBGL = recentBGL; } public boolean isUploaded() { @@ -147,6 +199,207 @@ public class PumpStatusEvent extends RealmObject { this.uploaded = uploaded; } + public boolean isOldSgvWhenNewExpected() { + return oldSgvWhenNewExpected; + } + + public void setOldSgvWhenNewExpected(boolean oldSgvWhenNewExpected) { + this.oldSgvWhenNewExpected = oldSgvWhenNewExpected; + } + + public boolean isValidPUMP() { + return validPUMP; + } + + public void setValidPUMP(boolean validPUMP) { + this.validPUMP = validPUMP; + } + + public boolean isValidCGM() { + return validCGM; + } + + public void setValidCGM(boolean validCGM) { + this.validCGM = validCGM; + } + + public boolean isValidSGV() { + return validSGV; + } + + public void setValidSGV(boolean validSGV) { + this.validSGV = validSGV; + } + + public boolean isValidBGL() { + return validBGL; + } + + public void setValidBGL(boolean validBGL) { + this.validBGL = validBGL; + } + + public boolean isValidBolus() { + return validBolus; + } + + public void setValidBolus(boolean validBolus) { + this.validBolus = validBolus; + } + + public boolean isValidBolusDual() { + return validBolusDual; + } + + public void setValidBolusDual(boolean validBolusDual) { + this.validBolusDual = validBolusDual; + } + + public boolean isValidBolusSquare() { + return validBolusSquare; + } + + public void setValidBolusSquare(boolean validBolusSquare) { + this.validBolusSquare = validBolusSquare; + } + + public boolean isValidALERT() { + return validALERT; + } + + public void setValidALERT(boolean validALERT) { + this.validALERT = validALERT; + } + + public boolean isValidSUSPEND() { + return validSUSPEND; + } + + public void setValidSUSPEND(boolean validSUSPEND) { + this.validSUSPEND = validSUSPEND; + } + + public boolean isValidSUSPENDOFF() { + return validSUSPENDOFF; + } + + public void setValidSUSPENDOFF(boolean validSUSPENDOFF) { + this.validSUSPENDOFF = validSUSPENDOFF; + } + + public Date getSuspendAfterDate() { + return suspendAfterDate; + } + + public void setSuspendAfterDate(Date suspendAfterDate) { + this.suspendAfterDate = suspendAfterDate; + } + + public Date getSuspendBeforeDate() { + return suspendBeforeDate; + } + + public void setSuspendBeforeDate(Date suspendBeforeDate) { + this.suspendBeforeDate = suspendBeforeDate; + } + + + public boolean isValidTEMPBASAL() { + return validTEMPBASAL; + } + + public void setValidTEMPBASAL(boolean validTEMPBASAL) { + this.validTEMPBASAL = validTEMPBASAL; + } + + public Date getTempBasalAfterDate() { + return tempBasalAfterDate; + } + + public void setTempBasalAfterDate(Date tempBasalAfterDate) { + this.tempBasalAfterDate = tempBasalAfterDate; + } + + public Date getTempBasalBeforeDate() { + return tempBasalBeforeDate; + } + + public void setTempBasalBeforeDate(Date tempBasalBeforeDate) { + this.tempBasalBeforeDate = tempBasalBeforeDate; + } + + public boolean isValidCAGE() { + return validCAGE; + } + + public void setValidCAGE(boolean validCAGE) { + this.validCAGE = validCAGE; + } + + public Date getCageAfterDate() { + return cageAfterDate; + } + + public void setCageAfterDate(Date cageAfterDate) { + this.cageAfterDate = cageAfterDate; + } + + public Date getCageBeforeDate() { + return cageBeforeDate; + } + + public void setCageBeforeDate(Date cageBeforeDate) { + this.cageBeforeDate = cageBeforeDate; + } + + public boolean isValidSAGE() { + return validSAGE; + } + + public void setValidSAGE(boolean validSAGE) { + this.validSAGE = validSAGE; + } + + public Date getSageAfterDate() { + return sageAfterDate; + } + + public void setSageAfterDate(Date sageAfterDate) { + this.sageAfterDate = sageAfterDate; + } + + public Date getSageBeforeDate() { + return sageBeforeDate; + } + + public void setSageBeforeDate(Date sageBeforeDate) { + this.sageBeforeDate = sageBeforeDate; + } + + public boolean isValidBATTERY() { + return validBATTERY; + } + + public void setValidBATTERY(boolean validBATTERY) { + this.validBATTERY = validBATTERY; + } + + public Date getBatteryAfterDate() { + return batteryAfterDate; + } + + public void setBatteryAfterDate(Date batteryAfterDate) { + this.batteryAfterDate = batteryAfterDate; + } + + public Date getBatteryBeforeDate() { + return batteryBeforeDate; + } + + public void setBatteryBeforeDate(Date batteryBeforeDate) { + this.batteryBeforeDate = batteryBeforeDate; + } + public boolean isSuspended() { return suspended; } @@ -155,12 +408,28 @@ public class PumpStatusEvent extends RealmObject { this.suspended = suspended; } - public boolean isBolusing() { - return bolusing; + public boolean isBolusingNormal() { + return bolusingNormal; + } + + public void setBolusingNormal(boolean bolusingNormal) { + this.bolusingNormal = bolusingNormal; + } + + public boolean isBolusingSquare() { + return bolusingSquare; + } + + public void setBolusingSquare(boolean bolusingSquare) { + this.bolusingSquare = bolusingSquare; } - public void setBolusing(boolean bolusing) { - this.bolusing = bolusing; + public boolean isBolusingDual() { + return bolusingDual; + } + + public void setBolusingDual(boolean bolusingDual) { + this.bolusingDual = bolusingDual; } public boolean isDeliveringInsulin() { @@ -211,11 +480,11 @@ public class PumpStatusEvent extends RealmObject { this.tempBasalRate = tempBasalRate; } - public byte getTempBasalPercentage() { + public short getTempBasalPercentage() { return tempBasalPercentage; } - public void setTempBasalPercentage(byte tempBasalPercentage) { + public void setTempBasalPercentage(short tempBasalPercentage) { this.tempBasalPercentage = tempBasalPercentage; } @@ -243,28 +512,28 @@ public class PumpStatusEvent extends RealmObject { this.minutesOfInsulinRemaining = minutesOfInsulinRemaining; } - public Date getSgvDate() { - return sgvDate; + public Date getCgmDate() { + return cgmDate; } - public void setSgvDate(Date sgvDate) { - this.sgvDate = sgvDate; + public void setCgmDate(Date cgmDate) { + this.cgmDate = cgmDate; } - public boolean isLowSuspendActive() { - return lowSuspendActive; + public Date getCgmPumpDate() { + return cgmPumpDate; } - public void setLowSuspendActive(boolean lowSuspendActive) { - this.lowSuspendActive = lowSuspendActive; + public void setCgmPumpDate(Date cgmPumpDate) { + this.cgmPumpDate = cgmPumpDate; } - public boolean isRecentBolusWizard() { - return recentBolusWizard; + public boolean isLowSuspendActive() { + return lowSuspendActive; } - public void setRecentBolusWizard(boolean recentBolusWizard) { - this.recentBolusWizard = recentBolusWizard; + public void setLowSuspendActive(boolean lowSuspendActive) { + this.lowSuspendActive = lowSuspendActive; } public long getPumpTimeOffset() { @@ -275,17 +544,202 @@ public class PumpStatusEvent extends RealmObject { this.pumpTimeOffset = pumpTimeOffset; } + public int getPumpStatus() { + return pumpStatus; + } + + public void setPumpStatus(byte pumpStatus) { + this.pumpStatus = pumpStatus; + } + + public int getCgmStatus() { + return cgmStatus; + } + + public void setCgmStatus(byte cgmStatus) { + this.cgmStatus = cgmStatus; + } + + public boolean isCgmCalibrating() { + return cgmCalibrating; + } + + public void setCgmCalibrating(boolean cgmCalibrating) { + this.cgmCalibrating = cgmCalibrating; + } + + public boolean isCgmCalibrationComplete() { + return cgmCalibrationComplete; + } + + public void setCgmCalibrationComplete(boolean cgmCalibrationComplete) { + this.cgmCalibrationComplete = cgmCalibrationComplete; + } + + public boolean isCgmException() { + return cgmException; + } + + public void setCgmException(boolean cgmException) { + this.cgmException = cgmException; + } + + public boolean isCgmWarmUp() { + return cgmWarmUp; + } + + public void setCgmWarmUp(boolean cgmWarmUp) { + this.cgmWarmUp = cgmWarmUp; + } + + public byte getCgmExceptionType() { + return cgmExceptionType; + } + + public void setCgmExceptionType(byte cgmExceptionType) { + this.cgmExceptionType = cgmExceptionType; + } + + public short getAlert() { + return alert; + } + + public void setAlert(short alert) { + this.alert = alert; + } + + public Date getAlertDate() { + return alertDate; + } + + public void setAlertDate(Date alertDate) { + this.alertDate = alertDate; + } + + public Date getAlertPumpDate() { + return alertPumpDate; + } + + public void setAlertPumpDate(Date alertPumpDate) { + this.alertPumpDate = alertPumpDate; + } + + public float getBolusingDelivered() { + return bolusingDelivered; + } + + public void setBolusingDelivered(float bolusingDelivered) { + this.bolusingDelivered = bolusingDelivered; + } + + public short getBolusingMinutesRemaining() { + return bolusingMinutesRemaining; + } + + public void setBolusingMinutesRemaining(short bolusingMinutesRemaining) { + this.bolusingMinutesRemaining = bolusingMinutesRemaining; + } + + public short getBolusingReference() { + return bolusingReference; + } + + public void setBolusingReference(short bolusingReference) { + this.bolusingReference = bolusingReference; + } + + public float getLastBolusAmount() { + return lastBolusAmount; + } + + public void setLastBolusAmount(float lastBolusAmount) { + this.lastBolusAmount = lastBolusAmount; + } + + public Date getLastBolusDate() { + return lastBolusDate; + } + + public void setLastBolusDate(Date lastBolusDate) { + this.lastBolusDate = lastBolusDate; + } + + public Date getLastBolusPumpDate() { + return lastBolusPumpDate; + } + + public void setLastBolusPumpDate(Date lastBolusPumpDate) { + this.lastBolusPumpDate = lastBolusPumpDate; + } + + public short getLastBolusReference() { + return lastBolusReference; + } + + public void setLastBolusReference(short lastBolusReference) { + this.lastBolusReference = lastBolusReference; + } + + public short getLastBolusDuration() { + return lastBolusDuration; + } + + public void setLastBolusDuration(short lastBolusDuration) { + this.lastBolusDuration = lastBolusDuration; + } + + public byte getTransmitterBattery() { + return transmitterBattery; + } + + public void setTransmitterBattery(byte transmitterBattery) { + this.transmitterBattery = transmitterBattery; + } + + public byte getTransmitterControl() { + return transmitterControl; + } + + public void setTransmitterControl(byte transmitterControl) { + this.transmitterControl = transmitterControl; + } + + public short getCalibrationDueMinutes() { + return calibrationDueMinutes; + } + + public void setCalibrationDueMinutes(short calibrationDueMinutes) { + this.calibrationDueMinutes = calibrationDueMinutes; + } + + public float getSensorRateOfChange() { + return sensorRateOfChange; + } + + public void setSensorRateOfChange(float sensorRateOfChange) { + this.sensorRateOfChange = sensorRateOfChange; + } + @Override public String toString() { return "PumpStatusEvent{" + "eventDate=" + eventDate + ", pumpDate=" + pumpDate + + ", pumpTimeOffset=" + pumpTimeOffset + ", deviceName='" + deviceName + '\'' + + ", pumpStatus=" + pumpStatus + + ", cgmStatus=" + cgmStatus + ", suspended=" + suspended + - ", bolusing=" + bolusing + + ", bolusingNormal=" + bolusingNormal + + ", bolusingSquare=" + bolusingSquare + + ", bolusingDual=" + bolusingDual + ", deliveringInsulin=" + deliveringInsulin + ", tempBasalActive=" + tempBasalActive + ", cgmActive=" + cgmActive + + ", cgmCalibrating=" + cgmCalibrating + + ", cgmCalibrationComplete=" + cgmCalibrationComplete + + ", cgmException=" + cgmException + + ", cgmWarmUp=" + cgmWarmUp + ", activeBasalPattern=" + activeBasalPattern + ", basalRate=" + basalRate + ", tempBasalRate=" + tempBasalRate + @@ -297,12 +751,53 @@ public class PumpStatusEvent extends RealmObject { ", minutesOfInsulinRemaining=" + minutesOfInsulinRemaining + ", activeInsulin=" + activeInsulin + ", sgv=" + sgv + - ", sgvDate=" + sgvDate + + ", cgmDate=" + cgmDate + + ", cgmPumpDate=" + cgmPumpDate + + ", cgmExceptionType=" + cgmExceptionType + ", lowSuspendActive=" + lowSuspendActive + ", cgmTrend='" + cgmTrend + '\'' + ", recentBolusWizard=" + recentBolusWizard + - ", bolusWizardBGL=" + bolusWizardBGL + - ", pumpTimeOffset=" + pumpTimeOffset + + ", recentBGL=" + recentBGL + + ", alert=" + alert + + ", alertDate=" + alertDate + + ", alertPumpDate=" + alertPumpDate + + ", bolusingDelivered=" + bolusingDelivered + + ", bolusingMinutesRemaining=" + bolusingMinutesRemaining + + ", bolusingReference=" + bolusingReference + + ", lastBolusAmount=" + lastBolusAmount + + ", lastBolusDate=" + lastBolusDate + + ", lastBolusPumpDate=" + lastBolusPumpDate + + ", lastBolusDuration=" + lastBolusDuration + + ", lastBolusReference=" + lastBolusReference + + ", transmitterBattery=" + transmitterBattery + + ", transmitterControl=" + transmitterControl + + ", calibrationDueMinutes=" + calibrationDueMinutes + + ", sensorRateOfChange=" + sensorRateOfChange + + ", oldSgvWhenNewExpected=" + oldSgvWhenNewExpected + + ", validPUMP=" + validPUMP + + ", validCGM=" + validCGM + + ", validSGV=" + validSGV + + ", validBGL=" + validBGL + + ", validBolus=" + validBolus + + ", validBolusDual=" + validBolusDual + + ", validBolusSquare=" + validBolusSquare + + ", validALERT=" + validALERT + + ", validSUSPEND=" + validSUSPEND + + ", validSUSPENDOFF=" + validSUSPENDOFF + + ", suspendAfterDate=" + suspendAfterDate + + ", suspendBeforeDate=" + suspendBeforeDate + + ", validTEMPBASAL=" + validTEMPBASAL + + ", tempBasalAfterDate=" + tempBasalAfterDate + + ", tempBasalBeforeDate=" + tempBasalBeforeDate + + ", validCAGE=" + validCAGE + + ", cageAfterDate=" + cageAfterDate + + ", cageBeforeDate=" + cageBeforeDate + + ", validSAGE=" + validSAGE + + ", sageAfterDate=" + sageAfterDate + + ", sageBeforeDate=" + sageBeforeDate + + ", validBATTERY=" + validBATTERY + + ", batteryAfterDate=" + batteryAfterDate + + ", batteryBeforeDate=" + batteryBeforeDate + ", uploaded=" + uploaded + '}'; } diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java index 819f82e3923c37694a2ba17447dac2d82543ee0b..06f7f79eccf7b415c588be5e9a65e966f5cf113a 100644 --- a/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java @@ -1,7 +1,5 @@ package info.nightscout.android.upload.nightscout; -import android.util.Log; - import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.security.MessageDigest; @@ -16,11 +14,22 @@ import info.nightscout.android.upload.nightscout.serializer.EntriesSerializer; import android.support.annotation.NonNull; +import info.nightscout.android.utils.ConfigurationStore; import info.nightscout.api.UploadApi; -import info.nightscout.api.GlucoseEndpoints; -import info.nightscout.api.BolusEndpoints.BolusEntry; -import info.nightscout.api.GlucoseEndpoints.GlucoseEntry; -import info.nightscout.api.BolusEndpoints; +import info.nightscout.api.SgvEndpoints; +import info.nightscout.api.SgvEndpoints.SgvEntry; +import info.nightscout.api.MbgEndpoints; +import info.nightscout.api.MbgEndpoints.MbgEntry; +import info.nightscout.api.TreatmentEndpoints; +import info.nightscout.api.TreatmentEndpoints.TreatmentEntry; +import info.nightscout.api.TempBasalRateEndpoints.TempBasalRateEntry; +import info.nightscout.api.TempBasalRateEndpoints; +import info.nightscout.api.TempBasalPercentEndpoints; +import info.nightscout.api.TempBasalPercentEndpoints.TempBasalPercentEntry; +import info.nightscout.api.TempBasalCancelEndpoints; +import info.nightscout.api.TempBasalCancelEndpoints.TempBasalCancelEntry; +import info.nightscout.api.NoteEndpoints; +import info.nightscout.api.NoteEndpoints.NoteEntry; import info.nightscout.api.DeviceEndpoints; import info.nightscout.api.DeviceEndpoints.Iob; import info.nightscout.api.DeviceEndpoints.Battery; @@ -30,39 +39,43 @@ import info.nightscout.api.DeviceEndpoints.DeviceStatus; import okhttp3.ResponseBody; import retrofit2.Response; +import static info.nightscout.android.medtronic.MainActivity.MMOLXLFACTOR; + class NightScoutUpload { private static final String TAG = NightscoutUploadIntentService.class.getSimpleName(); private static final SimpleDateFormat ISO8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + private static final SimpleDateFormat NOTE_DATE_FORMAT = new SimpleDateFormat("E h:mm a", Locale.getDefault()); NightScoutUpload() { } - Boolean doRESTUpload(String url, + boolean doRESTUpload(String url, String secret, + boolean treatments, int uploaderBatteryLevel, - List<PumpStatusEvent> records) { - Boolean success = false; - try { - success = isUploaded(records, url, secret, uploaderBatteryLevel); - } catch (Exception e) { - Log.e(TAG, "Unable to do REST API Upload to: " + url, e); - } - return success; + List<PumpStatusEvent> records) throws Exception { + return isUploaded(records, url, secret, treatments, uploaderBatteryLevel); } - private boolean isUploaded(List<PumpStatusEvent> records, String baseURL, String secret, + boolean treatments, int uploaderBatteryLevel) throws Exception { UploadApi uploadApi = new UploadApi(baseURL, formToken(secret)); - boolean eventsUploaded = uploadEvents(uploadApi.getGlucoseEndpoints(), - uploadApi.getBolusEndpoints(), - records); + boolean eventsUploaded = uploadEvents( + uploadApi.getSgvEndpoints(), + uploadApi.getMbgEndpoints(), + uploadApi.getTreatmentEndpoints(), + uploadApi.getTempBasalRateEndpoints(), + uploadApi.getTempBasalPercentEndpoints(), + uploadApi.getTempBasalCancelEndpoints(), + uploadApi.getNoteEndpoints(), + records, treatments); boolean deviceStatusUploaded = uploadDeviceStatus(uploadApi.getDeviceEndpoints(), uploaderBatteryLevel, records); @@ -70,48 +83,200 @@ class NightScoutUpload { return eventsUploaded && deviceStatusUploaded; } - private boolean uploadEvents(GlucoseEndpoints glucoseEndpoints, - BolusEndpoints bolusEndpoints, - List<PumpStatusEvent> records) throws Exception { - - - List<GlucoseEntry> glucoseEntries = new ArrayList<>(); - List<BolusEntry> bolusEntries = new ArrayList<>(); + private boolean uploadEvents(SgvEndpoints sgvEndpoints, + MbgEndpoints mbgEndpoints, + TreatmentEndpoints treatmentEndpoints, + TempBasalRateEndpoints tempBasalRateEndpoints, + TempBasalPercentEndpoints tempBasalPercentEndpoints, + TempBasalCancelEndpoints tempBasalCancelEndpoints, + NoteEndpoints noteEndpoints, + List<PumpStatusEvent> records, + boolean treatments) throws Exception { + + + List<SgvEntry> sgvEntries = new ArrayList<>(); + List<MbgEntry> mbgEntries = new ArrayList<>(); + List<TreatmentEntry> treatmentEntries = new ArrayList<>(); + List<TempBasalRateEntry> tempBasalRateEntries = new ArrayList<>(); + List<TempBasalPercentEntry> tempBasalPercentEntries = new ArrayList<>(); + List<TempBasalCancelEntry> tempBasalCancelEntries = new ArrayList<>(); + List<NoteEntry> noteEntries = new ArrayList<>(); for (PumpStatusEvent record : records) { - GlucoseEntry glucoseEntry = new GlucoseEntry(); - - glucoseEntry.setType("sgv"); - glucoseEntry.setDirection(EntriesSerializer.getDirectionStringStatus(record.getCgmTrend())); - glucoseEntry.setDevice(record.getDeviceName()); - glucoseEntry.setSgv(record.getSgv()); - glucoseEntry.setDate(record.getSgvDate().getTime()); - glucoseEntry.setDateString(record.getSgvDate().toString()); - - glucoseEntries.add(glucoseEntry); + if (record.isValidSGV()) { + SgvEntry sgvEntry = new SgvEntry(); + sgvEntry.setType("sgv"); + sgvEntry.setDirection(EntriesSerializer.getDirectionStringStatus(record.getCgmTrend())); + sgvEntry.setDevice(record.getDeviceName()); + sgvEntry.setSgv(record.getSgv()); + sgvEntry.setDate(record.getCgmDate().getTime()); + sgvEntry.setDateString(record.getCgmDate().toString()); + sgvEntries.add(sgvEntry); + } - if (record.getBolusWizardBGL() != 0) { - BolusEntry bolusEntry = new BolusEntry(); + if (record.isValidBGL()) { + + MbgEntry mbgEntry = new MbgEntry(); + mbgEntry.setType("mbg"); + mbgEntry.setDate(record.getEventDate().getTime()); + mbgEntry.setDateString(record.getEventDate().toString()); + mbgEntry.setDevice(record.getDeviceName()); + mbgEntry.setMbg(record.getRecentBGL()); + mbgEntries.add(mbgEntry); + + // cgm offline or not in use (needed for NS to show bgl when no sgv data) + if (!record.isCgmActive() || record.isCgmWarmUp()) { + ConfigurationStore configurationStore = ConfigurationStore.getInstance(); + BigDecimal bgl; + String units; + if (configurationStore.isMmolxl()) { + bgl = new BigDecimal(record.getRecentBGL() / MMOLXLFACTOR).setScale(1, BigDecimal.ROUND_HALF_UP); + units = "mmol"; + } else { + bgl = new BigDecimal(record.getRecentBGL()).setScale(0); + units = "mg/dl"; + } + + TreatmentEntry treatmentEntry = new TreatmentEntry(); + treatmentEntry.setCreatedAt(ISO8601_DATE_FORMAT.format(record.getEventDate())); + treatmentEntry.setEventType("BG Check"); + treatmentEntry.setGlucoseType("Finger"); + treatmentEntry.setGlucose(bgl); + treatmentEntry.setUnits(units); + treatmentEntries.add(treatmentEntry); + } + } - bolusEntry.setType("mbg"); - bolusEntry.setDate(record.getEventDate().getTime()); - bolusEntry.setDateString(record.getEventDate().toString()); - bolusEntry.setDevice(record.getDeviceName()); - bolusEntry.setMbg(record.getBolusWizardBGL()); + if (treatments) { + + if (record.isValidBolus()) { + + if (record.isValidBolusDual()) { + TreatmentEntry treatmentEntry = new TreatmentEntry(); + treatmentEntry.setCreatedAt(ISO8601_DATE_FORMAT.format(record.getLastBolusDate())); + treatmentEntry.setEventType("Bolus"); + treatmentEntry.setInsulin(record.getLastBolusAmount()); + treatmentEntry.setNotes("Dual bolus normal part delivered: " + record.getLastBolusAmount() + "u"); + treatmentEntries.add(treatmentEntry); + + } else if (record.isValidBolusSquare()) { + TreatmentEntry treatmentEntry = new TreatmentEntry(); + treatmentEntry.setCreatedAt(ISO8601_DATE_FORMAT.format(record.getLastBolusDate())); + treatmentEntry.setEventType("Combo Bolus"); + treatmentEntry.setDuration(record.getLastBolusDuration()); + treatmentEntry.setSplitNow("0"); + treatmentEntry.setSplitExt("100"); + treatmentEntry.setRelative(2); + treatmentEntry.setEnteredinsulin(String.valueOf(record.getLastBolusAmount())); + treatmentEntries.add(treatmentEntry); + + noteEntries.add(new NoteEntry( + "Announcement", + ISO8601_DATE_FORMAT.format(record.getLastBolusDate().getTime() + (record.getLastBolusDuration() * 60 * 1000)), + "Square bolus delivered: " + record.getLastBolusAmount() + "u Duration: " + record.getLastBolusDuration() + " minutes" + )); + + } else { + TreatmentEntry treatmentEntry = new TreatmentEntry(); + treatmentEntry.setCreatedAt(ISO8601_DATE_FORMAT.format(record.getLastBolusDate())); + treatmentEntry.setEventType("Bolus"); + treatmentEntry.setInsulin(record.getLastBolusAmount()); + treatmentEntries.add(treatmentEntry); + } + } + + if (record.isValidTEMPBASAL()) { + if (record.getTempBasalMinutesRemaining() > 0 && record.getTempBasalPercentage() > 0) { + tempBasalPercentEntries.add(new TempBasalPercentEntry( + ISO8601_DATE_FORMAT.format(record.getEventDate()), + "Temp Basal started approx: " + NOTE_DATE_FORMAT.format(record.getTempBasalAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getTempBasalBeforeDate()), + record.getTempBasalMinutesRemaining(), + record.getTempBasalPercentage() - 100 + )); + } else if (record.getTempBasalMinutesRemaining() > 0) { + tempBasalRateEntries.add(new TempBasalRateEntry( + ISO8601_DATE_FORMAT.format(record.getEventDate()), + "Temp Basal started approx: " + NOTE_DATE_FORMAT.format(record.getTempBasalAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getTempBasalBeforeDate()), + record.getTempBasalMinutesRemaining(), + record.getTempBasalRate() + )); + } else { + tempBasalCancelEntries.add(new TempBasalCancelEntry( + ISO8601_DATE_FORMAT.format(record.getEventDate()), + "Temp Basal stopped approx: " + NOTE_DATE_FORMAT.format(record.getTempBasalAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getTempBasalBeforeDate()) + )); + } + } + + if (record.isValidSUSPEND()) { + tempBasalRateEntries.add(new TempBasalRateEntry( + ISO8601_DATE_FORMAT.format(record.getEventDate()), + "Pump suspended insulin delivery approx: " + NOTE_DATE_FORMAT.format(record.getSuspendAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getSuspendBeforeDate()), + 60, + 0 + )); + } + if (record.isValidSUSPENDOFF()) { + tempBasalCancelEntries.add(new TempBasalCancelEntry( + ISO8601_DATE_FORMAT.format(record.getEventDate()), + "Pump resumed insulin delivery approx: " + NOTE_DATE_FORMAT.format(record.getSuspendAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getSuspendBeforeDate()) + )); + } + + if (record.isValidSAGE()) { + noteEntries.add(new NoteEntry( + "Sensor Start", + ISO8601_DATE_FORMAT.format(record.getSageAfterDate().getTime() - (record.getSageAfterDate().getTime() - record.getSageBeforeDate().getTime()) / 2), + "Sensor changed approx: " + NOTE_DATE_FORMAT.format(record.getSageAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getSageBeforeDate()) + )); + } + if (record.isValidCAGE()) { + noteEntries.add(new NoteEntry( + "Site Change", + ISO8601_DATE_FORMAT.format(record.getCageAfterDate().getTime() - (record.getCageAfterDate().getTime() - record.getCageBeforeDate().getTime()) / 2), + "Reservoir changed approx: " + NOTE_DATE_FORMAT.format(record.getCageAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getCageBeforeDate()) + )); + } + if (record.isValidBATTERY()) { + noteEntries.add(new NoteEntry( + "Note", + ISO8601_DATE_FORMAT.format(record.getBatteryAfterDate().getTime() - (record.getBatteryAfterDate().getTime() - record.getBatteryBeforeDate().getTime()) / 2), + "Pump battery changed approx: " + NOTE_DATE_FORMAT.format(record.getBatteryAfterDate()) + " - " + NOTE_DATE_FORMAT.format(record.getBatteryBeforeDate()) + )); + } - bolusEntries.add(bolusEntry); } } boolean uploaded = true; - if (glucoseEntries.size() > 0) { - Response<ResponseBody> result = glucoseEndpoints.sendEntries(glucoseEntries).execute(); + if (sgvEntries.size() > 0) { + Response<ResponseBody> result = sgvEndpoints.sendEntries(sgvEntries).execute(); uploaded = result.isSuccessful(); } - if (bolusEntries.size() > 0) { - Response<ResponseBody> result = bolusEndpoints.sendEntries(bolusEntries).execute(); + if (mbgEntries.size() > 0) { + Response<ResponseBody> result = mbgEndpoints.sendEntries(mbgEntries).execute(); + uploaded = uploaded && result.isSuccessful(); + } + if (treatmentEntries.size() > 0) { + Response<ResponseBody> result = treatmentEndpoints.sendEntries(treatmentEntries).execute(); + uploaded = uploaded && result.isSuccessful(); + } + if (tempBasalRateEntries.size() > 0) { + Response<ResponseBody> result = tempBasalRateEndpoints.sendEntries(tempBasalRateEntries).execute(); + uploaded = uploaded && result.isSuccessful(); + } + if (tempBasalPercentEntries.size() > 0) { + Response<ResponseBody> result = tempBasalPercentEndpoints.sendEntries(tempBasalPercentEntries).execute(); + uploaded = uploaded && result.isSuccessful(); + } + if (tempBasalCancelEntries.size() > 0) { + Response<ResponseBody> result = tempBasalCancelEndpoints.sendEntries(tempBasalCancelEntries).execute(); + uploaded = uploaded && result.isSuccessful(); + } + if (noteEntries.size() > 0) { + Response<ResponseBody> result = noteEndpoints.sendEntries(noteEntries).execute(); uploaded = uploaded && result.isSuccessful(); } return uploaded; @@ -125,21 +290,89 @@ class NightScoutUpload { List<DeviceStatus> deviceEntries = new ArrayList<>(); for (PumpStatusEvent record : records) { - Iob iob = new Iob(record.getPumpDate(), record.getActiveInsulin()); + Iob iob = new Iob(record.getEventDate(), record.getActiveInsulin()); Battery battery = new Battery(record.getBatteryPercentage()); PumpStatus pumpstatus; - if (record.isBolusing()) { - pumpstatus = new PumpStatus(true, false, ""); + // shorten pump status when needed to accommodate mobile browsers + boolean shorten = false; + if ((record.isBolusingSquare() || record.isBolusingDual()) && record.isTempBasalActive()) + shorten = true; + + String statusPUMP = "normal"; + if (record.isBolusingNormal()) { + statusPUMP = "bolusing"; } else if (record.isSuspended()) { - pumpstatus = new PumpStatus(false, true, ""); + statusPUMP = "suspended"; + } else { + if (record.isBolusingSquare()) { + if (shorten) + statusPUMP = "S>" + record.getBolusingDelivered() + "u-" + record.getBolusingMinutesRemaining() + "m"; + else + statusPUMP = "square>>" + record.getBolusingDelivered() + "u-" + (record.getBolusingMinutesRemaining() >= 60 ? record.getBolusingMinutesRemaining() / 60 + "h" : "") + record.getBolusingMinutesRemaining() % 60 + "m"; + shorten = true; + } else if (record.isBolusingDual()) { + if (shorten) + statusPUMP = "D>" + record.getBolusingDelivered() + "-" + record.getBolusingMinutesRemaining() + "m"; + else + statusPUMP = "dual>>" + record.getBolusingDelivered() + "u-" + (record.getBolusingMinutesRemaining() >= 60 ? record.getBolusingMinutesRemaining() / 60 + "h" : "") + record.getBolusingMinutesRemaining() % 60 + "m"; + shorten = true; + } + if (record.getTempBasalMinutesRemaining() > 0 & record.getTempBasalPercentage() != 0) { + if (shorten) + statusPUMP = " T>" + record.getTempBasalPercentage() + "%-" + record.getTempBasalMinutesRemaining() + "m"; + else + statusPUMP = "temp>>" + record.getTempBasalPercentage() + "%-" + (record.getTempBasalMinutesRemaining() >= 60 ? record.getTempBasalMinutesRemaining() / 60 + "h" : "") + record.getTempBasalMinutesRemaining() % 60 + "m"; + shorten = true; + } else if (record.getTempBasalMinutesRemaining() > 0) { + if (shorten) + statusPUMP = " T>" + record.getTempBasalRate() + "-" + record.getTempBasalMinutesRemaining() + "m"; + else + statusPUMP = "temp>>" + record.getTempBasalRate() + "u-" + (record.getTempBasalMinutesRemaining() >= 60 ? record.getTempBasalMinutesRemaining() / 60 + "h" : "") + record.getTempBasalMinutesRemaining() % 60 + "m"; + shorten = true; + } + } + + if (record.getAlert() > 0) + statusPUMP = "⚠ " + statusPUMP; + + String statusCGM = ""; + if (record.isCgmActive()) { + if (record.getTransmitterBattery() > 80) + statusCGM = shorten ? "" : ":::: "; + else if (record.getTransmitterBattery() > 55) + statusCGM = shorten ? "" : ":::. "; + else if (record.getTransmitterBattery() > 30) + statusCGM = shorten ? "" : "::.. "; + else if (record.getTransmitterBattery() > 10) + statusCGM = shorten ? "" : ":... "; + else + statusCGM = shorten ? "" : ".... "; + if (record.isCgmCalibrating()) + statusCGM += shorten ? "cal" : "calibrating"; + else if (record.isCgmCalibrationComplete()) + statusCGM += shorten ? "cal" : "cal.complete"; + else { + if (record.isCgmWarmUp()) + statusCGM += shorten ? "WU" : "warmup "; + if (record.getCalibrationDueMinutes() > 0) { + if (shorten) + statusCGM += (record.getCalibrationDueMinutes() >= 120 ? record.getCalibrationDueMinutes() / 60 + "h" : record.getCalibrationDueMinutes() + "m"); + else + statusCGM += (record.getCalibrationDueMinutes() >= 60 ? record.getCalibrationDueMinutes() / 60 + "h" : "") + record.getCalibrationDueMinutes() % 60 + "m"; + } + else + statusCGM += shorten ? "cal.now" : "calibrate now!"; + } } else { - pumpstatus = new PumpStatus(false, false, "normal"); + statusCGM += shorten ? "n/a" : "cgm n/a"; } + pumpstatus = new PumpStatus(false, false, statusPUMP + " " + statusCGM); + PumpInfo pumpInfo = new PumpInfo( - ISO8601_DATE_FORMAT.format(record.getPumpDate()), - new BigDecimal(record.getReservoirAmount()).setScale(3, BigDecimal.ROUND_HALF_UP), + ISO8601_DATE_FORMAT.format(record.getEventDate()), + new BigDecimal(record.getReservoirAmount()).setScale(0, BigDecimal.ROUND_HALF_UP), iob, battery, pumpstatus @@ -148,7 +381,7 @@ class NightScoutUpload { DeviceStatus deviceStatus = new DeviceStatus( uploaderBatteryLevel, record.getDeviceName(), - ISO8601_DATE_FORMAT.format(record.getPumpDate()), + ISO8601_DATE_FORMAT.format(record.getEventDate()), pumpInfo ); @@ -176,5 +409,4 @@ class NightScoutUpload { } return sb.toString(); } - } diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java index 038218934c13ccb0c2e99389977a5106ec8b536a..30dcd7cdc73e1c3bddebe863d3af2c5201ade573 100644 --- a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutUploadIntentService.java @@ -11,6 +11,7 @@ import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import info.nightscout.android.R; +import info.nightscout.android.medtronic.service.MedtronicCnlIntentService; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; import info.nightscout.android.utils.DataStore; import io.realm.Realm; @@ -29,8 +30,8 @@ public class NightscoutUploadIntentService extends IntentService { protected void sendStatus(String message) { Intent localIntent = - new Intent(Constants.ACTION_STATUS_MESSAGE) - .putExtra(Constants.EXTENDED_DATA, message); + new Intent(MedtronicCnlIntentService.Constants.ACTION_STATUS_MESSAGE) + .putExtra(MedtronicCnlIntentService.Constants.EXTENDED_DATA, message); LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent); } @@ -53,13 +54,14 @@ public class NightscoutUploadIntentService extends IntentService { RealmResults<PumpStatusEvent> records = mRealm .where(PumpStatusEvent.class) .equalTo("uploaded", false) - .notEqualTo("sgv", 0) .findAll(); if (records.size() > 0) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); Boolean enableRESTUpload = prefs.getBoolean("EnableRESTUpload", false); + Boolean enableTreatmentsUpload = prefs.getBoolean("EnableTreatmentsUpload", false); + try { if (enableRESTUpload) { long start = System.currentTimeMillis(); @@ -67,21 +69,30 @@ public class NightscoutUploadIntentService extends IntentService { String urlSetting = prefs.getString(mContext.getString(R.string.preference_nightscout_url), ""); String secretSetting = prefs.getString(mContext.getString(R.string.preference_api_secret), "YOURAPISECRET"); Boolean uploadSuccess = mNightScoutUpload.doRESTUpload(urlSetting, - secretSetting, DataStore.getInstance().getUploaderBatteryLevel(), records); + secretSetting, enableTreatmentsUpload, DataStore.getInstance().getUploaderBatteryLevel(), records); if (uploadSuccess) { mRealm.beginTransaction(); for (PumpStatusEvent updateRecord : records) { updateRecord.setUploaded(true); } mRealm.commitTransaction(); + } else { + sendStatus(MedtronicCnlIntentService.ICON_WARN + "Uploading to Nightscout returned unsuccessful"); } Log.i(TAG, String.format("Finished upload of %s record using a REST API in %s ms", records.size(), System.currentTimeMillis() - start)); + } else { + mRealm.beginTransaction(); + for (PumpStatusEvent updateRecord : records) { + updateRecord.setUploaded(true); + } + mRealm.commitTransaction(); } } catch (Exception e) { + sendStatus(MedtronicCnlIntentService.ICON_WARN + "Error uploading: " + e.getMessage()); Log.e(TAG, "ERROR uploading data!!!!!", e); } } else { - Log.i(TAG, "No records has to be uploaded"); + Log.i(TAG, "No records have to be uploaded"); } mRealm.close(); @@ -100,5 +111,4 @@ public class NightscoutUploadIntentService extends IntentService { public static final String EXTENDED_DATA = "info.nightscout.android.upload.nightscout.DATA"; } - } diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java b/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java index 1f40ab015abd146de2c596efb414ad798df58594..0b44575a4313565608b2c17d21e39ad5561ac568 100644 --- a/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/serializer/EntriesSerializer.java @@ -80,8 +80,8 @@ public class EntriesSerializer implements JsonSerializer<PumpStatusEvent> { jsonObject.addProperty("direction", getDirectionString(src.getCgmTrend())); jsonObject.addProperty("device", src.getDeviceName()); jsonObject.addProperty("type", "sgv"); - jsonObject.addProperty("date", src.getSgvDate().getTime()); - jsonObject.addProperty("dateString", String.valueOf(src.getSgvDate())); + jsonObject.addProperty("date", src.getCgmDate().getTime()); + jsonObject.addProperty("dateString", String.valueOf(src.getCgmDate())); return jsonObject; } diff --git a/app/src/main/java/info/nightscout/android/utils/DataStore.java b/app/src/main/java/info/nightscout/android/utils/DataStore.java index 86bfa944f893a0fa8925c7c0da7d6c7a00a2b73c..c6f24d69bfb4a71d813d6e54400edc5e92f9277a 100644 --- a/app/src/main/java/info/nightscout/android/utils/DataStore.java +++ b/app/src/main/java/info/nightscout/android/utils/DataStore.java @@ -1,13 +1,8 @@ package info.nightscout.android.utils; -import org.apache.commons.lang3.time.DateUtils; - import java.util.Date; -import info.nightscout.android.model.medtronicNg.PumpStatusEvent; -import io.realm.Realm; - /** * Created by volker on 30.03.2017. */ @@ -15,16 +10,21 @@ import io.realm.Realm; public class DataStore { private static DataStore instance; - private PumpStatusEvent lastPumpStatus; - private int uploaderBatteryLevel = 0; - private int unavailableSGVCount = 0; - private int lastBolusWizardBGL = 0; + private Date uploaderStartDate; + private long activePumpMac = 0; - private int commsErrorCount = 0; - private int commsSuccessCount = 0; - private int commsConnectThreshold = 0; - private int commsSignalThreshold = 0; - private float commsUnavailableThreshold = 0; + private int uploaderBatteryLevel = 0; + + private int pumpCgmNA = 0; + + private int CommsSuccess = 0; + private int CommsError = 0; + private int CommsConnectError = 0; + private int CommsSignalError = 0; + private int CommsSgvSuccess = 0; + private int PumpLostSensorError = 0; + private int PumpClockError = 0; + private int PumpBatteryError = 0; private DataStore() {} @@ -32,27 +32,21 @@ public class DataStore { if (DataStore.instance == null) { instance = new DataStore(); - // set some initial dummy values - PumpStatusEvent dummyStatus = new PumpStatusEvent(); - dummyStatus.setSgvDate(DateUtils.addDays(new Date(), -1)); - dummyStatus.setSgv(0); - - // bypass setter to avoid dealing with a real Realm object - instance.lastPumpStatus = dummyStatus; + instance.uploaderStartDate = new Date(); } - return instance; } - public PumpStatusEvent getLastPumpStatus() { - return lastPumpStatus; + public Date getUploaderStartDate() { + return uploaderStartDate; } - public void setLastPumpStatus(PumpStatusEvent lastPumpStatus) { - Realm realm = Realm.getDefaultInstance(); + public long getActivePumpMac() { + return activePumpMac; + } - this.lastPumpStatus = realm.copyFromRealm(lastPumpStatus); - if (!realm.isClosed()) realm.close(); + public void setActivePumpMac(long activePumpMac) { + this.activePumpMac = activePumpMac; } public int getUploaderBatteryLevel() { @@ -63,112 +57,109 @@ public class DataStore { this.uploaderBatteryLevel = uploaderBatteryLevel; } - public int getUnavailableSGVCount() { - return unavailableSGVCount; + public int getPumpCgmNA() { + return pumpCgmNA; } - public int incUnavailableSGVCount() { - return unavailableSGVCount++; + public int incPumpCgmNA() { + return pumpCgmNA++; } - public void clearUnavailableSGVCount() { - this.unavailableSGVCount = 0; + public void clearPumpCgmNA() { + this.pumpCgmNA = 0; } - public void setUnavailableSGVCount(int unavailableSGVCount) { - this.unavailableSGVCount = unavailableSGVCount; + public int getCommsSuccess() { + return CommsSuccess; } - public int getLastBolusWizardBGL() { - return lastBolusWizardBGL; - } + public int incCommsSuccess() { return CommsSuccess++; } - public void setLastBolusWizardBGL(int lastBolusWizardBGL) { - this.lastBolusWizardBGL = lastBolusWizardBGL; + public void clearCommsSuccess() { + this.CommsSuccess = 0; } - public long getActivePumpMac() { - return activePumpMac; + public int getCommsError() { + return CommsError; } - public void setActivePumpMac(long activePumpMac) { - this.activePumpMac = activePumpMac; + public int incCommsError() { return CommsError++; } + + public void clearCommsError() { + this.CommsError = 0; } - public int getCommsErrorCount() { - return commsErrorCount; + public int getCommsConnectError() { + return CommsConnectError; } - public int incCommsErrorCount() { return commsErrorCount++; } + public int incCommsConnectError() { return CommsConnectError++; } - public int decCommsErrorThreshold() { - if (commsErrorCount > 0) commsErrorCount--; - return commsErrorCount;} + public int decCommsConnectError() { + if (CommsConnectError > 0) CommsConnectError--; + return CommsConnectError;} - public void clearCommsErrorCount() { - this.commsErrorCount = 0; + public void clearCommsConnectError() { + this.CommsConnectError = 0; } - public int getCommsSuccessCount() { - return commsSuccessCount; + public int getCommsSignalError() { + return CommsSignalError; } - public int incCommsSuccessCount() { return commsSuccessCount++; } + public int incCommsSignalError() { return CommsSignalError++; } - public int decCommsSuccessCount() { - if (commsSuccessCount > 0) commsSuccessCount--; - return commsSuccessCount;} + public int decCommsSignalError() { + if (CommsSignalError > 0) CommsSignalError--; + return CommsSignalError;} - public void clearCommsSuccessCount() { - this.commsSuccessCount = 0; + public void clearCommsSignalError() { + this.CommsSignalError = 0; } - public int getCommsConnectThreshold() { - return commsConnectThreshold; + public int getCommsSgvSuccess() { + return CommsSgvSuccess; } - public int incCommsConnectThreshold() { return commsConnectThreshold++; } - - public int decCommsConnectThreshold() { - if (commsConnectThreshold > 0) commsConnectThreshold--; - return commsConnectThreshold;} + public int incCommsSgvSuccess() { return CommsSgvSuccess++; } - public void clearCommsConnectThreshold() { - this.commsConnectThreshold = 0; + public int getPumpLostSensorError() { + return PumpLostSensorError; } - public int getCommsSignalThreshold() { - return commsSignalThreshold; + public int incPumpLostSensorError() { return PumpLostSensorError++; } + + public void clearPumpLostSensorError() { + this.PumpLostSensorError = 0; } - public int incCommsSignalThreshold() { return commsSignalThreshold++; } + public int getPumpClockError() { + return PumpClockError; + } - public int decCommsSignalThreshold() { - if (commsSignalThreshold > 0) commsSignalThreshold--; - return commsSignalThreshold;} + public int incPumpClockError() { return PumpClockError++; } - public void clearCommsSignalThreshold() { - this.commsSignalThreshold = 0; + public void clearPumpClockError() { + this.PumpClockError = 0; } - public float getCommsUnavailableThreshold() { - return commsUnavailableThreshold; + public int getPumpBatteryError() { + return PumpBatteryError; } - public float addCommsUnavailableThreshold(float value) { - commsUnavailableThreshold+= value; - if (commsUnavailableThreshold < 0) commsUnavailableThreshold = 0; - return commsUnavailableThreshold;} + public int incPumpBatteryError() { return PumpBatteryError++; } - public void clearCommsUnavailableThreshold() { - this.commsUnavailableThreshold = 0; + public void clearPumpBatteryError() { + this.PumpBatteryError = 0; } public void clearAllCommsErrors() { - this.commsErrorCount = 0; - this.commsSuccessCount = 0; - this.commsConnectThreshold = 0; - this.commsSignalThreshold = 0; - this.commsUnavailableThreshold = 0; + this.CommsSuccess = 0; + this.CommsError = 0; + this.CommsConnectError = 0; + this.CommsSignalError = 0; + this.PumpLostSensorError = 0; + this.PumpClockError = 0; + this.PumpBatteryError = 0; } } diff --git a/app/src/main/java/info/nightscout/android/utils/StatusStore.java b/app/src/main/java/info/nightscout/android/utils/StatusStore.java new file mode 100644 index 0000000000000000000000000000000000000000..fd0e39e4dd553abf4f794c0910a8f7774f064ea8 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/utils/StatusStore.java @@ -0,0 +1,27 @@ +package info.nightscout.android.utils; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import io.realm.RealmObject; +import io.realm.annotations.Index; + +public class StatusStore extends RealmObject { + @Index + private long timestamp; + private String message; + + public void StatusMessage(String message) { + StatusMessage(System.currentTimeMillis(), message); + } + + public void StatusMessage(long timestamp, String message) { + this.timestamp = timestamp; + this.message = message; + } + + public String toString() { + DateFormat df = new SimpleDateFormat("E HH:mm:ss"); + return df.format(timestamp) + ": " + message; + } +} diff --git a/app/src/main/java/info/nightscout/android/xdrip_plus/XDripPlusUploadIntentService.java b/app/src/main/java/info/nightscout/android/xdrip_plus/XDripPlusUploadIntentService.java index e06a88b8cf047dde4d5a812b58198e69bd508573..22be13663c6931536562a3aa7ecb7ee20c4c4089 100644 --- a/app/src/main/java/info/nightscout/android/xdrip_plus/XDripPlusUploadIntentService.java +++ b/app/src/main/java/info/nightscout/android/xdrip_plus/XDripPlusUploadIntentService.java @@ -61,7 +61,9 @@ public class XDripPlusUploadIntentService extends IntentService { RealmResults<PumpStatusEvent> all_records = mRealm .where(PumpStatusEvent.class) - .notEqualTo("sgv", 0) + .equalTo("validSGV", true) + .or() + .equalTo("validBGL", true) .findAllSorted("eventDate", Sort.DESCENDING); // get the most recent record and send that @@ -117,14 +119,14 @@ public class XDripPlusUploadIntentService extends IntentService { JSONObject json = new JSONObject(); json.put("uploaderBattery", DataStore.getInstance().getUploaderBatteryLevel()); json.put("device", record.getDeviceName()); - json.put("created_at", ISO8601_DATE_FORMAT.format(record.getPumpDate())); + json.put("created_at", ISO8601_DATE_FORMAT.format(record.getEventDate())); JSONObject pumpInfo = new JSONObject(); - pumpInfo.put("clock", ISO8601_DATE_FORMAT.format(record.getPumpDate())); + pumpInfo.put("clock", ISO8601_DATE_FORMAT.format(record.getEventDate())); pumpInfo.put("reservoir", new BigDecimal(record.getReservoirAmount()).setScale(3, BigDecimal.ROUND_HALF_UP)); JSONObject iob = new JSONObject(); - iob.put("timestamp", record.getPumpDate()); + iob.put("timestamp", record.getEventDate()); iob.put("bolusiob", record.getActiveInsulin()); JSONObject battery = new JSONObject(); @@ -139,25 +141,27 @@ public class XDripPlusUploadIntentService extends IntentService { } private void addSgvEntry(JSONArray entriesArray, PumpStatusEvent pumpRecord) throws Exception { - JSONObject json = new JSONObject(); - // TODO replace with Retrofit/EntriesSerializer - json.put("sgv", pumpRecord.getSgv()); - json.put("direction", EntriesSerializer.getDirectionString(pumpRecord.getCgmTrend())); - json.put("device", pumpRecord.getDeviceName()); - json.put("type", "sgv"); - json.put("date", pumpRecord.getSgvDate().getTime()); - json.put("dateString", pumpRecord.getSgvDate()); - - entriesArray.put(json); + if (pumpRecord.isValidSGV()) { + JSONObject json = new JSONObject(); + // TODO replace with Retrofit/EntriesSerializer + json.put("sgv", pumpRecord.getSgv()); + json.put("direction", EntriesSerializer.getDirectionString(pumpRecord.getCgmTrend())); + json.put("device", pumpRecord.getDeviceName()); + json.put("type", "sgv"); + json.put("date", pumpRecord.getCgmDate().getTime()); + json.put("dateString", pumpRecord.getCgmDate()); + + entriesArray.put(json); + } } private void addMbgEntry(JSONArray entriesArray, PumpStatusEvent pumpRecord) throws Exception { - if (pumpRecord.hasRecentBolusWizard()) { + if (pumpRecord.isValidBGL()) { JSONObject json = new JSONObject(); // TODO replace with Retrofit/EntriesSerializer json.put("type", "mbg"); - json.put("mbg", pumpRecord.getBolusWizardBGL()); + json.put("mbg", pumpRecord.getRecentBGL()); json.put("device", pumpRecord.getDeviceName()); json.put("date", pumpRecord.getEventDate().getTime()); json.put("dateString", pumpRecord.getEventDate()); diff --git a/app/src/main/java/info/nightscout/api/BolusEndpoints.java b/app/src/main/java/info/nightscout/api/MbgEndpoints.java similarity index 63% rename from app/src/main/java/info/nightscout/api/BolusEndpoints.java rename to app/src/main/java/info/nightscout/api/MbgEndpoints.java index a1c2a56e35eacaa534b5e5ede7de4fca577672c8..9c443342275e0b708b7beaef4c8c489375483479 100644 --- a/app/src/main/java/info/nightscout/api/BolusEndpoints.java +++ b/app/src/main/java/info/nightscout/api/MbgEndpoints.java @@ -8,57 +8,36 @@ import retrofit2.http.Body; import retrofit2.http.Headers; import retrofit2.http.POST; -public interface BolusEndpoints { +public interface MbgEndpoints { - class BolusEntry { + class MbgEntry { String type; String dateString; long date; int mbg; String device; - public BolusEntry() { } - - public String getType() { - return type; - } + public MbgEntry() { } public void setType(String type) { this.type = type; } - public String getDateString() { - return dateString; - } - public void setDateString(String dateString) { this.dateString = dateString; } - public long getDate() { - return date; - } - public void setDate(long date) { this.date = date; } - public int getMbg() { - return mbg; - } - public void setMbg(int mbg) { this.mbg = mbg; } - public String getDevice() { - return device; - } - public void setDevice(String device) { this.device = device; } - } @Headers({ @@ -66,8 +45,7 @@ public interface BolusEndpoints { "Content-type: application/json" }) @POST("/api/v1/entries") - Call<ResponseBody> sendEntries(@Body List<BolusEntry> entries); - + Call<ResponseBody> sendEntries(@Body List<MbgEntry> entries); } diff --git a/app/src/main/java/info/nightscout/api/NoteEndpoints.java b/app/src/main/java/info/nightscout/api/NoteEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..cd51af7b265ff20902c7c09b8d007e7c67c26836 --- /dev/null +++ b/app/src/main/java/info/nightscout/api/NoteEndpoints.java @@ -0,0 +1,31 @@ +package info.nightscout.api; + +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface NoteEndpoints { + + class NoteEntry { + String eventType; + String created_at; + String notes; + + public NoteEntry(String eventType, String created_at, String notes) { + this.eventType = eventType; + this.created_at = created_at; + this.notes = notes; + } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/treatments") + Call<ResponseBody> sendEntries(@Body List<NoteEntry> noteEntries); +} diff --git a/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java b/app/src/main/java/info/nightscout/api/SgvEndpoints.java similarity index 62% rename from app/src/main/java/info/nightscout/api/GlucoseEndpoints.java rename to app/src/main/java/info/nightscout/api/SgvEndpoints.java index d34c5e7abdae5efadc346bc560eb54aee252dacb..96dd7bb376d5e73ad325bea640d134ab66144ce5 100644 --- a/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java +++ b/app/src/main/java/info/nightscout/api/SgvEndpoints.java @@ -8,10 +8,9 @@ import retrofit2.http.Body; import retrofit2.http.Headers; import retrofit2.http.POST; -public interface GlucoseEndpoints { - - class GlucoseEntry { +public interface SgvEndpoints { + class SgvEntry { String type; String dateString; long date; @@ -19,55 +18,31 @@ public interface GlucoseEndpoints { String direction; String device; - public String getType() { - return type; - } - public void setType(String type) { this.type = type; } - public String getDateString() { - return dateString; - } - public void setDateString(String dateString) { this.dateString = dateString; } - public long getDate() { - return date; - } - public void setDate(long date) { this.date = date; } - public int getSgv() { - return sgv; - } - public void setSgv(int sgv) { this.sgv = sgv; } - public String getDirection() { - return direction; - } - public void setDirection(String direction) { this.direction = direction; } - public String getDevice() { - return device; - } - public void setDevice(String device) { this.device = device; } - public GlucoseEntry() { } + public SgvEntry() { } } @Headers({ @@ -75,8 +50,7 @@ public interface GlucoseEndpoints { "Content-type: application/json" }) @POST("/api/v1/entries") - Call<ResponseBody> sendEntries(@Body List<GlucoseEntry> entries); - + Call<ResponseBody> sendEntries(@Body List<SgvEntry> entries); } diff --git a/app/src/main/java/info/nightscout/api/TempBasalCancelEndpoints.java b/app/src/main/java/info/nightscout/api/TempBasalCancelEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..1d0ca98de5268200421ccd369f45f439966bb2cc --- /dev/null +++ b/app/src/main/java/info/nightscout/api/TempBasalCancelEndpoints.java @@ -0,0 +1,31 @@ +package info.nightscout.api; + +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface TempBasalCancelEndpoints { + + class TempBasalCancelEntry { + String eventType = "Temp Basal"; + String created_at; + String notes; + float duration = 0; + + public TempBasalCancelEntry(String created_at, String notes) { + this.created_at = created_at; + this.notes = notes; + } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/treatments") + Call<ResponseBody> sendEntries(@Body List<TempBasalCancelEntry> entries); +} diff --git a/app/src/main/java/info/nightscout/api/TempBasalPercentEndpoints.java b/app/src/main/java/info/nightscout/api/TempBasalPercentEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..2622b3c1804c8a512c90cf7eaca74cf607d72b16 --- /dev/null +++ b/app/src/main/java/info/nightscout/api/TempBasalPercentEndpoints.java @@ -0,0 +1,34 @@ +package info.nightscout.api; + +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface TempBasalPercentEndpoints { + + class TempBasalPercentEntry { + String eventType = "Temp Basal"; + String created_at; + String notes; + float duration; + float percent; + + public TempBasalPercentEntry(String created_at, String notes, float duration, float percent) { + this.created_at = created_at; + this.notes = notes; + this.duration = duration; + this.percent = percent; + } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/treatments") + Call<ResponseBody> sendEntries(@Body List<TempBasalPercentEntry> entries); +} diff --git a/app/src/main/java/info/nightscout/api/TempBasalRateEndpoints.java b/app/src/main/java/info/nightscout/api/TempBasalRateEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..27f7d4ff9fbd7e91d4502ead0e14ebe93e494d5b --- /dev/null +++ b/app/src/main/java/info/nightscout/api/TempBasalRateEndpoints.java @@ -0,0 +1,34 @@ +package info.nightscout.api; + +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface TempBasalRateEndpoints { + + class TempBasalRateEntry { + String eventType = "Temp Basal"; + String created_at; + String notes; + float duration; + float absolute; + + public TempBasalRateEntry(String created_at, String notes, float duration, float absolute) { + this.created_at = created_at; + this.notes = notes; + this.duration = duration; + this.absolute = absolute; + } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/treatments") + Call<ResponseBody> sendEntries(@Body List<TempBasalRateEntry> entries); +} diff --git a/app/src/main/java/info/nightscout/api/TreatmentEndpoints.java b/app/src/main/java/info/nightscout/api/TreatmentEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..129af210241ddda4230f8401b41647a9ed4d69bf --- /dev/null +++ b/app/src/main/java/info/nightscout/api/TreatmentEndpoints.java @@ -0,0 +1,88 @@ +package info.nightscout.api; + +import java.math.BigDecimal; +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface TreatmentEndpoints { + + class TreatmentEntry { + String eventType; + String created_at; + String enteredinsulin; + String splitNow; + String splitExt; + String units; + String glucoseType; + String notes; + String device; + float insulin; + float duration; + float relative; + BigDecimal glucose; + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + public void setCreatedAt(String created_at) { + this.created_at = created_at; + } + + public void setDevice(String device) { + this.device = device; + } + + public void setInsulin(float insulin) { + this.insulin = insulin; + } + + public void setGlucose(BigDecimal glucose) { + this.glucose = glucose; + } + + public void setDuration(float duration) { + this.duration = duration; + } + + public void setRelative(float relative) { + this.relative = relative; + } + + public void setEnteredinsulin(String enteredinsulin) { + this.enteredinsulin = enteredinsulin; + } + + public void setSplitNow(String splitNow) { + this.splitNow = splitNow; + } + + public void setSplitExt(String splitExt) { + this.splitExt = splitExt; + } + + public void setUnits(String units) { + this.units = units; + } + + public void setGlucoseType(String glucoseType) { + this.glucoseType = glucoseType; + } + + public void setNotes(String notes) { + this.notes = notes; + } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/treatments") + Call<ResponseBody> sendEntries(@Body List <TreatmentEntry> entries); +} diff --git a/app/src/main/java/info/nightscout/api/UploadApi.java b/app/src/main/java/info/nightscout/api/UploadApi.java index a2c4a1a322a6c897c2e49b40b52f1ab52ee536a5..70ce741c202ab545cbbd805b2fa429dab1887551 100644 --- a/app/src/main/java/info/nightscout/api/UploadApi.java +++ b/app/src/main/java/info/nightscout/api/UploadApi.java @@ -14,21 +14,39 @@ import retrofit2.converter.gson.GsonConverterFactory; public class UploadApi { private Retrofit retrofit; - private GlucoseEndpoints glucoseEndpoints; - private BolusEndpoints bolusEndpoints; + private SgvEndpoints sgvEndpoints; + private MbgEndpoints mbgEndpoints; private DeviceEndpoints deviceEndpoints; - - public GlucoseEndpoints getGlucoseEndpoints() { - return glucoseEndpoints; + private TreatmentEndpoints treatmentEndpoints; + private TempBasalRateEndpoints tempBasalRateEndpoints; + private TempBasalPercentEndpoints tempBasalPercentEndpoints; + private TempBasalCancelEndpoints tempBasalCancelEndpoints; + private NoteEndpoints noteEndpoints; + + public SgvEndpoints getSgvEndpoints() { + return sgvEndpoints; } - - public BolusEndpoints getBolusEndpoints() { - return bolusEndpoints; + public MbgEndpoints getMbgEndpoints() { + return mbgEndpoints; } - public DeviceEndpoints getDeviceEndpoints() { return deviceEndpoints; } + public TreatmentEndpoints getTreatmentEndpoints() { + return treatmentEndpoints; + } + public TempBasalRateEndpoints getTempBasalRateEndpoints() { + return tempBasalRateEndpoints; + } + public TempBasalPercentEndpoints getTempBasalPercentEndpoints() { + return tempBasalPercentEndpoints; + } + public TempBasalCancelEndpoints getTempBasalCancelEndpoints() { + return tempBasalCancelEndpoints; + } + public NoteEndpoints getNoteEndpoints() { + return noteEndpoints; + } public UploadApi(String baseURL, String token) { @@ -70,8 +88,13 @@ public class UploadApi { .addConverterFactory(GsonConverterFactory.create()) .build(); - glucoseEndpoints = retrofit.create(GlucoseEndpoints.class); - bolusEndpoints = retrofit.create(BolusEndpoints.class); + sgvEndpoints = retrofit.create(SgvEndpoints.class); + mbgEndpoints = retrofit.create(MbgEndpoints.class); deviceEndpoints = retrofit.create(DeviceEndpoints.class); + treatmentEndpoints = retrofit.create(TreatmentEndpoints.class); + tempBasalRateEndpoints = retrofit.create(TempBasalRateEndpoints.class); + tempBasalPercentEndpoints = retrofit.create(TempBasalPercentEndpoints.class); + tempBasalCancelEndpoints = retrofit.create(TempBasalCancelEndpoints.class); + noteEndpoints = retrofit.create(NoteEndpoints.class); } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8d2fad1e9ffb117584baa409ab72db8c361d45df..6e9a1e7becfca0a268a7e11f0e3da233ade109a4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -66,7 +66,7 @@ android:gravity="bottom|center_horizontal" android:orientation="vertical"> - <TextView + <com.mikepenz.iconics.view.IconicsTextView android:id="@+id/textview_trend" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -135,14 +135,56 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="-10sp" + android:layout_gravity="center_horizontal" + android:orientation="horizontal"> + + <Button + android:id="@+id/button_log_top" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="older log entries" /> + + <Button + android:id="@+id/button_log_top_recent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="most recent" /> + </LinearLayout> + <com.mikepenz.iconics.view.IconicsTextView android:id="@+id/textview_log" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10sp" - android:maxLines="800" android:gravity="bottom" - android:text="" /> + android:text="" + android:layout_weight="0.33" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="-10sp" + android:layout_gravity="center_horizontal" + android:orientation="horizontal"> + + <Button + android:id="@+id/button_log_bottom" + android:layout_gravity="center_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="newer log entries" /> + + <Button + android:id="@+id/button_log_bottom_recent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="most recent" /> + </LinearLayout> + </LinearLayout> </ScrollView> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index ff89a6f2bb0e9027c0f4fc857a79595b7a6fe4d5..d590b78082d9dcf96ba6f0d30679d45121be3e77 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -19,12 +19,12 @@ android:switchTextOn="2" android:title="Decimals"/> <ListPreference - android:key="pollInterval" android:defaultValue="300000" - android:title="@string/preferences_poll_interval" - android:summary="%s" android:entries="@array/poll_interval" - android:entryValues="@array/poll_interval_millis"/> + android:entryValues="@array/poll_interval_millis" + android:key="pollInterval" + android:summary="%s" + android:title="@string/preferences_poll_interval" /> <info.nightscout.android.utils.CustomSwitchPreference android:disableDependentsState="false" android:key="doublePollOnPumpAway" @@ -70,6 +70,10 @@ android:dialogTitle="Enter your Nightscout API secret" android:key="@string/preference_api_secret" android:title="API Secret"/> + <CheckBoxPreference android:title="Treatments" + android:key="EnableTreatmentsUpload" + android:dependency="@string/preference_enable_rest_upload" + android:summary="Enable upload of treatments to Nightscout"/> <Preference android:title="scan NS-Autoconfig QR-Code" android:key="scanButton" android:dependency="@string/preference_enable_rest_upload"