diff --git a/app/build.gradle b/app/build.gradle index 742d781f311bc64d8087b7db300892313e22c6af..22026ff2431f6f1adbbadb1c698a7d798c73a8cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -145,7 +145,7 @@ release { dependencies { compile files('libs/slf4j-api-1.7.2.jar') - compile('com.crashlytics.sdk.android:crashlytics:2.6.5@aar') { + compile('com.crashlytics.sdk.android:crashlytics:2.6.6@aar') { transitive = true; } compile('com.mikepenz:materialdrawer:5.2.9@aar') { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ea896197d19abb365b6bbd2baaa8357cdd3692d..82d30493197be0106b2cf30795d7fed79e1ec57d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,11 +31,13 @@ <!-- I have set screenOrientation to "portrait" to avoid the restart of AsyncTasks when you rotate the phone --> + <!-- configChanges="uiMode" added to avoid restart of AsyncTasks when phone is plugged into charger/dock (for phones that can do usb otg & charging simultaneously --> <activity android:name=".medtronic.MainActivity" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:launchMode="singleTask" + android:configChanges="uiMode" android:screenOrientation="portrait"> <intent-filter android:icon="@drawable/ic_launcher"> 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 681f50ee06f600d809b4b987fb57f55366ffcebc..220ba9f2db5213d998fd4ca935afdcc7a39418c5 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java @@ -58,6 +58,7 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Queue; @@ -94,8 +95,12 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc private boolean hasZoomedChart = false; private NumberFormat sgvFormatter; - private boolean mmolxl; - private boolean mmolxlDecimals; + private static boolean mmolxl; + private static boolean mmolxlDecimals; + + public static long timeLastGoodSGV = 0; + public static short pumpBattery = 0; + public static int countUnavailableSGV = 0; boolean mEnableCgmService = true; SharedPreferences prefs = null; @@ -139,6 +144,20 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc return nextPoll; } + public static String strFormatSGV(float sgvValue) { + if (mmolxl) { + NumberFormat sgvFormatter; + if (mmolxlDecimals) { + sgvFormatter = new DecimalFormat("0.00"); + } else { + sgvFormatter = new DecimalFormat("0.0"); + } + return sgvFormatter.format(sgvValue / MMOLXLFACTOR); + } else { + return String.valueOf(sgvValue); + } + } + @Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "onCreate called"); @@ -332,17 +351,14 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc mChart.getGridLabelRenderer().setHumanRounding(false); mChart.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() { - DateFormat mFormat = DateFormat.getTimeInstance(DateFormat.SHORT); + DateFormat mFormat = new SimpleDateFormat("HH:mm"); // 24 hour format forced to fix label overlap + @Override public String formatLabel(double value, boolean isValueX) { if (isValueX) { return mFormat.format(new Date((long) value)); } else { - if (mmolxl) { - return sgvFormatter.format(value / MMOLXLFACTOR); - } else { return sgvFormatter.format(value); - } } }} ); @@ -617,7 +633,8 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc lastQueryTS = pump.getLastQueryTS(); - startCgmService(MainActivity.getNextPoll(pumpStatusData)); +// >>>>> note: prototype smart poll handling added to cnl intent +// startCgmService(MainActivity.getNextPoll(pumpStatusData)); // Delete invalid or old records from Realm // TODO - show an error message if the valid records haven't been uploaded @@ -640,7 +657,10 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } // TODO - handle isOffline in NightscoutUploadIntentService? - uploadCgmData(); + + // >>>>> check this out as it's uploading before cnl comms finishes and may cause occasional channel changes due to wifi noise - cnl intent handles ns upload trigger after all comms finish + // uploadCgmData(); + refreshDisplay(); } }); @@ -685,7 +705,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } } - private Queue<StatusMessage> messages = new ArrayBlockingQueue<>(10); + private Queue<StatusMessage> messages = new ArrayBlockingQueue<>(400); @Override public void onReceive(Context context, Intent intent) { @@ -693,7 +713,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc Log.i(TAG, "Message Receiver: " + message); synchronized (messages) { - while (messages.size() > 8) { + while (messages.size() > 398) { messages.poll(); } messages.add(new StatusMessage(message)); @@ -802,6 +822,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } private void updateChart(RealmResults<PumpStatusEvent> results) { + + mChart.getGridLabelRenderer().setNumHorizontalLabels(6); + int size = results.size(); if (size == 0) { final long now = System.currentTimeMillis(), @@ -832,9 +855,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc int sgv = pumpStatus.getSgv(); if (mmolxl) { - entries[pos++] = new DataPoint(pumpStatus.getEventDate(), pumpStatus.getSgv() / MMOLXLFACTOR); + entries[pos++] = new DataPoint(pumpStatus.getEventDate(), (float) pumpStatus.getSgv() / MMOLXLFACTOR); } else { - entries[pos++] = new DataPoint(pumpStatus.getEventDate(), pumpStatus.getSgv()); + entries[pos++] = new DataPoint(pumpStatus.getEventDate(), (float) pumpStatus.getSgv()); } } @@ -860,11 +883,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc double sgv = dataPoint.getY(); StringBuilder sb = new StringBuilder(mFormat.format(new Date((long) dataPoint.getX())) + ": "); - if (mmolxl) { - sb.append(sgvFormatter.format(sgv / MMOLXLFACTOR)); - } else { - sb.append(sgvFormatter.format(sgv)); - } + sb.append(sgvFormatter.format(sgv)); Toast.makeText(getBaseContext(), sb.toString(), Toast.LENGTH_SHORT).show(); } }); @@ -873,11 +892,11 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc @Override public void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint) { double sgv = dataPoint.getY(); - if (sgv < 80) + if (sgv < (mmolxl?4.5:80)) paint.setColor(Color.RED); - else if (sgv <= 180) + else if (sgv <= (mmolxl?10:180)) paint.setColor(Color.GREEN); - else if (sgv <= 260) + else if (sgv <= (mmolxl?14:260)) paint.setColor(Color.YELLOW); else paint.setColor(Color.RED); diff --git a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlReader.java b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlReader.java index 7efd3ef64e16fd2a735bb172950a83c2be3de732..6763c82d683bcd7daf32ba61f95294216a4a2a70 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlReader.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlReader.java @@ -9,7 +9,6 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; -import java.util.Locale; import java.util.concurrent.TimeoutException; import info.nightscout.android.USB.UsbHidDriver; @@ -51,6 +50,8 @@ public class MedtronicCnlReader { private MedtronicCnlSession mPumpSession = new MedtronicCnlSession(); private String mStickSerial = null; + private static final int SLEEP_MS = 500; + public MedtronicCnlReader(UsbHidDriver device) { mDevice = device; } @@ -78,9 +79,9 @@ public class MedtronicCnlReader { doRetry = false; try { new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.NAK) - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.EOT); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.EOT); new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.ENQ) - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); } catch (UnexpectedMessageException e2) { try { new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT).send(mDevice); @@ -95,11 +96,11 @@ public class MedtronicCnlReader { public void enterPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { Log.d(TAG, "Begin enterPasshtroughMode"); new ContourNextLinkCommandMessage("W|") - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); new ContourNextLinkCommandMessage("Q|") - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); new ContourNextLinkCommandMessage("1|") - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); Log.d(TAG, "Finished enterPasshtroughMode"); } @@ -150,9 +151,11 @@ public class MedtronicCnlReader { ChannelNegotiateResponseMessage response = new ChannelNegotiateRequestMessage(mPumpSession).send(mDevice); if (response.getRadioChannel() == mPumpSession.getRadioChannel()) { + mPumpSession.setRadioRSSI(response.getRadioRSSI()); break; } else { mPumpSession.setRadioChannel((byte)0); + mPumpSession.setRadioRSSI((byte)0); } } @@ -168,7 +171,17 @@ public class MedtronicCnlReader { public Date getPumpTime() throws EncryptionException, IOException, ChecksumException, TimeoutException, UnexpectedMessageException { Log.d(TAG, "Begin getPumpTime"); - // FIXME - throw if not in EHSM mode (add a state machine) + + // CNL<-->PUMP comms can have occasional short lived noise causing errors, retrying once catches this + try { + PumpTimeResponseMessage response = new PumpTimeRequestMessage(mPumpSession).send(mDevice); + Log.d(TAG, "Finished getPumpTime with date " + response.getPumpTime()); + return response.getPumpTime(); + } catch (UnexpectedMessageException e) { + Log.e(TAG, "Unexpected Message", e); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); + } PumpTimeResponseMessage response = new PumpTimeRequestMessage(mPumpSession).send(mDevice); @@ -179,7 +192,18 @@ public class MedtronicCnlReader { public PumpStatusEvent updatePumpStatus(PumpStatusEvent pumpRecord) throws IOException, EncryptionException, ChecksumException, TimeoutException, UnexpectedMessageException { Log.d(TAG, "Begin updatePumpStatus"); - // FIXME - throw if not in EHSM mode (add a state machine) + // CNL<-->PUMP comms can have occasional short lived noise causing errors, retrying once catches this + try { + PumpStatusResponseMessage response = new PumpStatusRequestMessage(mPumpSession).send(mDevice); + response.updatePumpRecord(pumpRecord); + Log.d(TAG, "Finished updatePumpStatus"); + return pumpRecord; + } catch (UnexpectedMessageException e) { + Log.e(TAG, "Unexpected Message", e); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); + } + PumpStatusResponseMessage response = new PumpStatusRequestMessage(mPumpSession).send(mDevice); response.updatePumpRecord(pumpRecord); @@ -222,18 +246,18 @@ public class MedtronicCnlReader { public void endPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { Log.d(TAG, "Begin endPassthroughMode"); new ContourNextLinkCommandMessage("W|") - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); new ContourNextLinkCommandMessage("Q|") - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); new ContourNextLinkCommandMessage("0|") - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); Log.d(TAG, "Finished endPassthroughMode"); } public void endControlMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { Log.d(TAG, "Begin endControlMode"); new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT) - .send(mDevice, 500).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ENQ); + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ENQ); Log.d(TAG, "Finished endControlMode"); } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlSession.java b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlSession.java index b5d96589c39013cbba4acf9a29c219dd9adfd65e..9c7952f3803a8a17437d51345a26be47dec1d092 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlSession.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlSession.java @@ -20,6 +20,8 @@ public class MedtronicCnlSession { private long pumpMAC; private byte radioChannel; + private byte radioRSSI; + private int bayerSequenceNumber = 1; private int medtronicSequenceNumber = 1; @@ -80,6 +82,14 @@ public class MedtronicCnlSession { return radioChannel; } + public byte getRadioRSSI() { + return radioRSSI; + } + + public int getRadioRSSIpercentage() { + return (((int) radioRSSI & 0x00FF) * 100) / 0xA8; + } + public void incrBayerSequenceNumber() { bayerSequenceNumber++; } @@ -92,6 +102,10 @@ public class MedtronicCnlSession { this.radioChannel = radioChannel; } + public void setRadioRSSI(byte radioRSSI) { + this.radioRSSI = radioRSSI; + } + public void setHMAC(byte[] hmac) { this.HMAC = hmac; } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateResponseMessage.java index 67f8d9beba4495cc760b07748f9141a576b66ee5..54cff417066ebfb51163b38a485f447d1e55ae64 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateResponseMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateResponseMessage.java @@ -16,6 +16,7 @@ public class ChannelNegotiateResponseMessage extends ContourNextLinkBinaryRespon private static final String TAG = ChannelNegotiateResponseMessage.class.getSimpleName(); private byte radioChannel = 0; + private byte radioRSSI = 0; protected ChannelNegotiateResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, IOException { super(payload); @@ -25,15 +26,21 @@ public class ChannelNegotiateResponseMessage extends ContourNextLinkBinaryRespon Log.d(TAG, "negotiateChannel: Check response length"); if (responseBytes.length > 46) { radioChannel = responseBytes[76]; + radioRSSI = responseBytes[59]; if (responseBytes[76] != pumpSession.getRadioChannel()) { throw new IOException(String.format(Locale.getDefault(), "Expected to get a message for channel %d. Got %d", pumpSession.getRadioChannel(), responseBytes[76])); } } else { radioChannel = ((byte) 0); + radioRSSI = ((byte) 0); } } public byte getRadioChannel() { return radioChannel; } + + public byte getRadioRSSI() { + return radioRSSI; + } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkMessage.java index 0d358090e414caec209005169b6d089cd38f7cf8..8c340cb9ef425cdd34e8bf322e2eef4aaa791926 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkMessage.java @@ -8,9 +8,7 @@ import java.nio.ByteBuffer; import java.util.concurrent.TimeoutException; import info.nightscout.android.USB.UsbHidDriver; -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.medtronic.MainActivity; import info.nightscout.android.utils.HexDump; /** @@ -20,7 +18,7 @@ public abstract class ContourNextLinkMessage { private static final String TAG = ContourNextLinkMessage.class.getSimpleName(); private static final int USB_BLOCKSIZE = 64; - private static final int READ_TIMEOUT_MS = 10000; + private static final int READ_TIMEOUT_MS = 15000; //ASTM standard is 15 seconds (note was previously set at 10 seconds) private static final String BAYER_USB_HEADER = "ABC"; protected ByteBuffer mPayload; @@ -142,6 +140,56 @@ public abstract class ContourNextLinkMessage { return responseMessage.toByteArray(); } + // safety check to make sure a expected 0x81 response is received before next expected 0x80 response + // very infrequent as clearMessage catches most issues but very important to save a CNL error situation + + protected int readMessage_0x81(UsbHidDriver mDevice) throws IOException, TimeoutException { + + int responseSize = 0; + boolean doRetry; + do { + byte[] responseBytes = readMessage(mDevice); + if (responseBytes[18] != (byte) 0x81) { + doRetry = true; + Log.d(TAG, "readMessage0x81: did not get 0x81 response, got " + responseBytes[18]); + } else { + doRetry = false; + responseSize = responseBytes.length; + } + + } while (doRetry); + + return responseSize; + } + + // intercept unexpected messages from the CNL + // these usually come from pump requests as it can occasionally resend message responses several times (possibly due to a missed CNL ACK during CNL-PUMP comms?) + // mostly noted on the higher radio channels, channel 26 shows this the most + // if these messages are not cleared the CNL will likely error needing to be unplugged to reset as it expects them to be read before any further commands are sent + + protected int clearMessage(UsbHidDriver mDevice) throws IOException { + + byte[] responseBuffer = new byte[USB_BLOCKSIZE]; + int bytesRead; + int bytesClear = 0; + + do { + bytesRead = mDevice.read(responseBuffer, 2000); + if (bytesRead > 0) { + bytesClear += bytesRead; + String responseString = HexDump.dumpHexString(responseBuffer); + Log.d(TAG, "READ: " + responseString); + } + } while (bytesRead > 0); + + if (bytesClear > 0) { + Log.d(TAG, "clearMessage: message stream cleared bytes: " + bytesClear); + } + + return bytesClear; + } + + public enum ASCII { STX(0x02), EOT(0x04), diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/EHSMMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/EHSMMessage.java index d930a59e3ae785b1bfec5255b3f914c1ffb2422f..0e064536b650efd76e2ee8dad5867a787671339f 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/EHSMMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/EHSMMessage.java @@ -20,6 +20,10 @@ public class EHSMMessage extends MedtronicSendMessageRequestMessage<ContourNext @Override public ContourNextLinkResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, UnexpectedMessageException { + + // clear unexpected incoming messages + clearMessage(mDevice); + sendMessage(mDevice); if (millis > 0) { try { @@ -27,11 +31,17 @@ public class EHSMMessage extends MedtronicSendMessageRequestMessage<ContourNext } catch (InterruptedException e) { } } + // The End EHSM Session only has an 0x81 response + if (readMessage_0x81(mDevice) != 48) { + throw new UnexpectedMessageException("length of EHSMMessage response does not match"); + } +/* readMessage(mDevice); if (this.encode().length != 54) { throw new UnexpectedMessageException("length of EHSMMessage response does not match"); } +*/ return null; } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusRequestMessage.java index 982bdba7bb025a7dff5d690d190a2926f66da227..39403a8f995610813fc4dcb1c7b22b4a69f28d5d 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusRequestMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/PumpStatusRequestMessage.java @@ -22,6 +22,7 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage } public PumpStatusResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException { + sendMessage(mDevice); if (millis > 0) { try { @@ -31,7 +32,7 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage } } // Read the 0x81 - readMessage(mDevice); + readMessage_0x81(mDevice); if (millis > 0) { try { Log.d(TAG, "waiting " + millis +" ms"); @@ -41,6 +42,9 @@ public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage } PumpStatusResponseMessage response = this.getResponse(readMessage(mDevice)); + // clear unexpected incoming messages + clearMessage(mDevice); + return response; } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeRequestMessage.java index 24cbb144d87a530c034e5666127346298e3fcf70..592800406888cb5c6ce3860642c785b274ce79a2 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeRequestMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeRequestMessage.java @@ -19,6 +19,7 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P @Override public PumpTimeResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException { + sendMessage(mDevice); if (millis > 0) { try { @@ -27,7 +28,7 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P } } // Read the 0x81 - readMessage(mDevice); + readMessage_0x81(mDevice); if (millis > 0) { try { Thread.sleep(millis); @@ -37,6 +38,9 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P // Read the 0x80 PumpTimeResponseMessage response = this.getResponse(readMessage(mDevice)); + // Pump sends additional 0x80 message when not using EHSM, lets clear this and any unexpected incoming messages + clearMessage(mDevice); + return response; } @@ -44,4 +48,4 @@ public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<P protected PumpTimeResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException, UnexpectedMessageException { return new PumpTimeResponseMessage(mPumpSession, payload); } -} +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeResponseMessage.java index 06d88ce206d0d88fa28fbda2bfb313c0d61131d6..2005880b7929d3dbd808d1f7be6e6592f07b1948 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeResponseMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/PumpTimeResponseMessage.java @@ -10,6 +10,7 @@ import info.nightscout.android.BuildConfig; import info.nightscout.android.medtronic.MedtronicCnlSession; 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.utils.HexDump; /** @@ -20,14 +21,14 @@ public class PumpTimeResponseMessage extends MedtronicSendMessageResponseMessage private Date pumpTime; - protected PumpTimeResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { + protected PumpTimeResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, UnexpectedMessageException { super(pumpSession, payload); if (this.encode().length < (61 + 8)) { // Invalid message. Return an invalid date. // TODO - deal with this more elegantly Log.e(TAG, "Invalid message received for getPumpTime"); - pumpTime = new Date(); + throw new UnexpectedMessageException("Invalid message received for getPumpTime"); } else { ByteBuffer dateBuffer = ByteBuffer.allocate(8); dateBuffer.order(ByteOrder.BIG_ENDIAN); diff --git a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmManager.java b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmManager.java index 240a2bfe44d853781c7d44f79dbae445d04555df..08737107d34864e8164ba6e04f25f4b327ed4c5a 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmManager.java +++ b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmManager.java @@ -80,7 +80,8 @@ public class MedtronicCnlAlarmManager { // restarting the alarm after MedtronicCnlIntentService.POLL_PERIOD_MS from now public static void restartAlarm() { - setAlarmAfterMillis(MainActivity.pollInterval + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS); + //setAlarmAfterMillis(MainActivity.pollInterval + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS); + setAlarmAfterMillis(MainActivity.pollInterval); // grace already accounted for when using current intent time to set default restart } // Cancel the alarm. 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 23fdd5dcf4a4fba01066e5e9b1ac38c6792e48e1..f76773d627f11b38b4ebb8ff136dde7850abb112 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 @@ -20,6 +20,8 @@ import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeoutException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import info.nightscout.android.R; import info.nightscout.android.USB.UsbHidDriver; @@ -98,6 +100,25 @@ public class MedtronicCnlIntentService extends IntentService { protected void onHandleIntent(Intent intent) { Log.d(TAG, "onHandleIntent called"); + long timePollStarted = System.currentTimeMillis(); + long timePollExpected = timePollStarted; + if (MainActivity.timeLastGoodSGV != 0) { + timePollExpected = MainActivity.timeLastGoodSGV + POLL_PERIOD_MS + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((timePollStarted - 1000L - (MainActivity.timeLastGoodSGV + POLL_GRACE_PERIOD_MS)) / POLL_PERIOD_MS)); + } + + // avoid polling when too close to sensor-pump comms + if (((timePollExpected - timePollStarted) > 5000L) && ((timePollExpected - timePollStarted) < (POLL_GRACE_PERIOD_MS + 45000L))) { + sendStatus("Please wait: Poll due in " + ((timePollExpected - timePollStarted) / 1000L) + " seconds"); + MedtronicCnlAlarmManager.setAlarm(timePollExpected); + MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); + return; + } + + long pollInterval = MainActivity.pollInterval; + if ((MainActivity.pumpBattery > 0) && (MainActivity.pumpBattery <= 25)) { + pollInterval = MainActivity.lowBatteryPollInterval; + } + if (!hasUsbHostFeature()) { sendStatus("It appears that this device doesn't support USB OTG."); Log.e(TAG, "Device does not support USB OTG"); @@ -135,14 +156,16 @@ public class MedtronicCnlIntentService extends IntentService { return; } + DateFormat df = new SimpleDateFormat("HH:mm:ss"); + MedtronicCnlReader cnlReader = new MedtronicCnlReader(mHidDevice); Realm realm = Realm.getDefaultInstance(); realm.beginTransaction(); try { - sendStatus("Connecting to the Contour Next Link..."); - Log.d(TAG, "Connecting to the Contour Next Link."); + sendStatus("Connecting to Contour Next Link"); + Log.d(TAG, "Connecting to Contour Next Link"); cnlReader.requestDeviceInfo(); // Is the device already configured? @@ -162,6 +185,7 @@ public class MedtronicCnlIntentService extends IntentService { try { cnlReader.enterPassthroughMode(); cnlReader.openConnection(); + cnlReader.requestReadInfo(); String key = info.getKey(); @@ -193,17 +217,12 @@ public class MedtronicCnlIntentService extends IntentService { if (radioChannel == 0) { sendStatus("Could not communicate with the 640g. Are you near the pump?"); Log.i(TAG, "Could not communicate with the 640g. Are you near the pump?"); - - // reduce polling interval to half until pump is available - MedtronicCnlAlarmManager.setAlarm(activePump.getLastQueryTS() + - (MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L)) - ); + pollInterval = MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L); // reduce polling interval to half until pump is available } else { setActivePumpMac(pumpMAC); activePump.setLastRadioChannel(radioChannel); - sendStatus(String.format(Locale.getDefault(), "Connected to Contour Next Link on channel %d.", (int) radioChannel)); + sendStatus(String.format(Locale.getDefault(), "Connected on channel %d RSSI: %d%%", (int) radioChannel, cnlReader.getPumpSession().getRadioRSSIpercentage())); Log.d(TAG, String.format("Connected to Contour Next Link on channel %d.", (int) radioChannel)); - cnlReader.beginEHSMSession(); // read pump status PumpStatusEvent pumpRecord = realm.createObject(PumpStatusEvent.class); @@ -223,13 +242,28 @@ public class MedtronicCnlIntentService extends IntentService { pumpRecord.setPumpDate(new Date(pumpTime - pumpOffset)); cnlReader.updatePumpStatus(pumpRecord); - cnlReader.endEHSMSession(); - if (pumpRecord.getSgv() != 0) { + + String offsetSign = ""; + if (pumpOffset > 0) { + offsetSign = "+"; + } + sendStatus("SGV: " + MainActivity.strFormatSGV(pumpRecord.getSgv()) + " At: " + df.format(pumpRecord.getEventDate().getTime()) + " Pump: " + offsetSign + (pumpOffset / 1000L) + "sec"); //note: event time is currently stored with offset + + // Check if pump sent old event when new expected and schedule a re-poll + if (((pumpRecord.getEventDate().getTime() - MainActivity.timeLastGoodSGV) < 5000L) && ((timePollExpected - timePollStarted) < 5000L)) { + pollInterval = 90000L; // polling interval set to 90 seconds + sendStatus("Pump sent old SGV event, re-polling..."); + } + + MainActivity.timeLastGoodSGV = pumpRecord.getEventDate().getTime(); // track last good sgv event time + MainActivity.pumpBattery = pumpRecord.getBatteryPercentage(); // track pump battery + MainActivity.countUnavailableSGV = 0; // reset unavailable sgv count + // Check that the record doesn't already exist before committing RealmResults<PumpStatusEvent> checkExistingRecords = activePump.getPumpHistory() .where() - .equalTo("eventDate", pumpRecord.getEventDate()) + .equalTo("eventDate", pumpRecord.getEventDate()) // >>>>>>> check as event date may not = exact pump event date due to it being stored with offset added this could lead to dup events due to slight variability in time offset .equalTo("sgv", pumpRecord.getSgv()) .findAll(); @@ -240,15 +274,24 @@ public class MedtronicCnlIntentService extends IntentService { Log.d(TAG, "history reading size: " + activePump.getPumpHistory().size()); Log.d(TAG, "history reading date: " + activePump.getPumpHistory().last().getEventDate()); + } else { + sendStatus("SGV: unavailable from pump"); + MainActivity.countUnavailableSGV ++; // poll clash detection } realm.commitTransaction(); // Tell the Main Activity we have new data sendMessage(Constants.ACTION_UPDATE_PUMP); } + } catch (UnexpectedMessageException e) { Log.e(TAG, "Unexpected Message", e); sendStatus("Communication Error: " + e.getMessage()); + pollInterval = MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); + sendStatus("Timeout communicating with the Contour Next Link."); + pollInterval = MainActivity.pollInterval / (MainActivity.reducePollOnPumpAway?2L:1L); } catch (NoSuchAlgorithmException e) { Log.e(TAG, "Could not determine CNL HMAC", e); sendStatus("Error connecting to Contour Next Link: Hashing error."); @@ -283,10 +326,34 @@ public class MedtronicCnlIntentService extends IntentService { } realm.close(); } - // TODO - set status if offline or Nightscout not reachable sendToXDrip(); uploadToNightscout(); + + // smart polling and pump-sensor poll clash detection + long lastActualPollTime = timePollStarted; + if (MainActivity.timeLastGoodSGV > 0) { + lastActualPollTime = MainActivity.timeLastGoodSGV + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((System.currentTimeMillis() - (MainActivity.timeLastGoodSGV + POLL_GRACE_PERIOD_MS)) / POLL_PERIOD_MS)); + } + long nextActualPollTime = lastActualPollTime + POLL_PERIOD_MS; + long nextRequestedPollTime = lastActualPollTime + pollInterval; + if ((nextRequestedPollTime - System.currentTimeMillis()) < 10000L) { + 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 (MainActivity.countUnavailableSGV > 0) { + if (MainActivity.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 (MainActivity.countUnavailableSGV > 2) { + sendStatus("Warning: No SGV available from pump for " + MainActivity.countUnavailableSGV + " attempts"); + nextRequestedPollTime += ((long) ((MainActivity.countUnavailableSGV - 2) % 5)) * (POLL_PERIOD_MS / 10L); // adjust poll time in 1/10 steps to avoid potential poll clash (max adjustment at 5/10) + } + } + MedtronicCnlAlarmManager.setAlarm(nextRequestedPollTime); + sendStatus("Next poll due at: " + df.format(nextRequestedPollTime)); + MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); } } 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 23335545a77d68efc80fd96e5a562e16e293ff51..eef010f25bcd95a441250f3090c01fd3d367e4e3 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 @@ -70,6 +70,7 @@ public class XDripPlusUploadIntentService extends IntentService { List<PumpStatusEvent> records = all_records.subList(0, 1); doXDripUpload(records); } + mRealm.close(); XDripPlusUploadReceiver.completeWakefulIntent(intent); } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 95148622918e6cc996d1ce7e5c3075d7ff3e399a..3dfa26586c1b258eca12ab5d0ba8e3f142d02d82 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -128,18 +128,21 @@ android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="fill_parent"> + android:gravity="bottom" <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> + android:gravity="bottom" <TextView android:id="@+id/textview_log" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10sp" - android:maxLines="20" + android:maxLines="800" + android:gravity="bottom" android:text="" /> </LinearLayout> </ScrollView>