diff --git a/.gitignore b/.gitignore index a0722a1d135612b37f3243c980473c0408b138e9..007d64268841d3fb3282a82e2376522174a3d543 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ .gradle -app/build -build/generated build -workspace.xml fabric.properties local.properties .idea bugfender.properties +/app/app.iml +gradle.properties \ No newline at end of file diff --git a/640gAndroidUploader.iml b/640gAndroidUploader.iml index dd11c422309587fbfb2c40dcfbc1c5c64bb0d573..21ad486e1dacdc31dfcf484957078ef9fbc29307 100644 --- a/640gAndroidUploader.iml +++ b/640gAndroidUploader.iml @@ -13,7 +13,7 @@ <content url="file://$MODULE_DIR$"> <excludeFolder url="file://$MODULE_DIR$/.gradle" /> </content> - <orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" /> + <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> </component> </module> \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 321863d615cb36775b630d58e4edd1721a5f7a06..6bfea65e09c18951c4bedbba1cb835ca18935ecb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'io.fabric.tools:gradle:1.21.6' - classpath 'io.realm:realm-gradle-plugin:1.1.1' + classpath 'io.realm:realm-gradle-plugin:2.3.1' classpath 'org.ajoberstar:grgit:1.5.0' } } @@ -28,7 +28,7 @@ apply plugin: 'realm-android' def gitVersion() { // current dir is <your proj>/app, so it's likely that all your git repo files are in the dir // above. - ext.repo = Grgit.open(project.file('..')) + ext.repo = Grgit.open() // should result in the same value as running // git tag -l | wc -l or git tag -l | grep -c ".*" - @@ -37,14 +37,15 @@ def gitVersion() { } def gitCommitId() { - //def process = ['sh', '-c', 'git tag -l | grep -c ".*" -'].execute().text.trim() - //return process.toInteger() + 1 - //return 42 - // current dir is <your proj>/app, so it's likely that all your git repo files are in the dir - // above. - ext.repo = Grgit.open(project.file('..')) + ext.repo = Grgit.open() + + return ext.repo.log().first().id.substring(0, 7) //+ " " + ext.repo.branch.current.name +} - return ext.repo.log().first().id.substring(0, 7) + +def gitBranch() { + ext.repo = Grgit.open() + return ext.repo.branch.current.name } def getBugfenderApiKey() { @@ -56,7 +57,7 @@ def getBugfenderApiKey() { android { compileSdkVersion 23 buildToolsVersion "23.0.3" - // FIXME - replace with URLConnection. This is used in GetHmacAndKeyActivity. + // FIXME - replace with URLConnection. This is used in ManageCNLActivity. useLibrary 'org.apache.http.legacy' applicationVariants.all { variant -> @@ -67,7 +68,7 @@ android { applicationId "info.nightscout.android" minSdkVersion 14 targetSdkVersion 23 - versionName project.properties['version'] + "/" + gitCommitId() + versionName project.properties['version'] + "/" + gitCommitId() // + " (" + gitBranch()+")" versionCode gitVersion() buildConfigField "String", "BUGFENDER_API_KEY", getBugfenderApiKey() } @@ -144,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.7@aar') { transitive = true; } compile('com.mikepenz:materialdrawer:5.2.9@aar') { @@ -154,11 +155,16 @@ dependencies { compile 'org.apache.commons:commons-lang3:3.4' compile 'com.mikepenz:google-material-typeface:2.2.0.1.original@aar' compile 'uk.co.chrisjenx:calligraphy:2.2.0' - compile 'com.bugfender.sdk:android:0.6.2' - compile 'com.github.PhilJay:MPAndroidChart:v3.0.0-beta1' - compile 'com.github.PhilJay:MPAndroidChart-Realm:v1.1.0@aar' + compile 'com.bugfender.sdk:android:0.7.2' + compile 'com.jjoe64:graphview:4.2.1' compile 'com.android.support:support-v4:23.4.0' compile 'com.google.code.gson:gson:2.7' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.squareup.okhttp3:okhttp:3.3.1' + compile 'com.squareup.okhttp3:logging-interceptor:3.3.1' + compile 'com.google.android.gms:play-services-appindexing:8.4.0' + + + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7d7185b04799440fc232b3ffd10ef632e76220c..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"> @@ -63,10 +65,8 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <activity - android:name=".medtronic.GetHmacAndKeyActivity" - android:label="@string/title_activity_login" - android:theme="@style/SettingsTheme" /> + + <activity android:name=".medtronic.ManageCNLActivity"/> <activity android:name=".medtronic.StatusActivity" /> @@ -84,6 +84,11 @@ <receiver android:name=".upload.nightscout.NightscoutUploadReceiver" /> <receiver android:name=".xdrip_plus.XDripPlusUploadReceiver" /> + + <receiver android:name=".medtronic.service.MedtronicCnlAlarmReceiver"></receiver> + + + </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/USB/USBPower.java b/app/src/main/java/info/nightscout/android/USB/USBPower.java index 92890b8e0f3577c2a6f4966b2f002da4b65a57ff..cf16a178a2ba8d5b6d8b4da4b13d1138f1391d4d 100644 --- a/app/src/main/java/info/nightscout/android/USB/USBPower.java +++ b/app/src/main/java/info/nightscout/android/USB/USBPower.java @@ -1,9 +1,9 @@ package info.nightscout.android.USB; -import java.io.DataOutputStream; - import android.util.Log; +import java.io.DataOutputStream; + public class USBPower { private static final String TAG = "USBPower"; diff --git a/app/src/main/java/info/nightscout/android/UploaderApplication.java b/app/src/main/java/info/nightscout/android/UploaderApplication.java index aa1944885e68507b24370f9bf2f2b75fe774bda1..1bfc87f81ded988434835da9d0af962dce3ff079 100644 --- a/app/src/main/java/info/nightscout/android/UploaderApplication.java +++ b/app/src/main/java/info/nightscout/android/UploaderApplication.java @@ -41,7 +41,8 @@ public class UploaderApplication extends Application { Bugfender.setDeviceString("NightscoutURL", prefs.getString(getString(R.string.preference_nightscout_url), "Not set")); } - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder(this) + Realm.init(this); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder() .deleteRealmIfMigrationNeeded() .build(); diff --git a/app/src/main/java/info/nightscout/android/medtronic/GetHmacAndKeyActivity.java b/app/src/main/java/info/nightscout/android/medtronic/GetHmacAndKeyActivity.java deleted file mode 100644 index dfd43740a7f4ce86fafde82fc746d661476fd109..0000000000000000000000000000000000000000 --- a/app/src/main/java/info/nightscout/android/medtronic/GetHmacAndKeyActivity.java +++ /dev/null @@ -1,130 +0,0 @@ -package info.nightscout.android.medtronic; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.TargetApi; -import android.app.LoaderManager.LoaderCallbacks; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Loader; -import android.database.Cursor; -import android.graphics.Color; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.text.Html; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.TextView; - -import com.mikepenz.google_material_typeface_library.GoogleMaterial; -import com.mikepenz.iconics.IconicsDrawable; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.util.EntityUtils; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.ArrayList; -import java.util.List; - -import info.nightscout.android.R; -import info.nightscout.android.medtronic.message.MessageUtils; -import info.nightscout.android.model.medtronicNg.ContourNextLinkInfo; -import io.realm.Realm; -import io.realm.RealmResults; -import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper; - -/** - * A login screen that offers login via username/password. - */ -public class GetHmacAndKeyActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> { - - // UI references. - private TextView mRegisteredStickView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_login); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle("Registered Devices"); - - mRegisteredStickView = (TextView) findViewById(R.id.registered_usb_devices); - - showRegisteredSticks(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - break; - } - return true; - } - - @Override - protected void attachBaseContext(Context newBase) { - super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)); - } - - private void showRegisteredSticks() { - Realm realm = Realm.getDefaultInstance(); - - RealmResults<ContourNextLinkInfo> results = realm.where(ContourNextLinkInfo.class).findAll(); - - String deviceTableHtml = ""; - - for (ContourNextLinkInfo info : results) { - String longSerial = info.getSerialNumber(); - String key = info.getKey(); - - deviceTableHtml += String.format("<b>Serial Number:</b> %s %s<br/>", longSerial, key == null ? "✘" : "✔"); - } - - mRegisteredStickView.setText(Html.fromHtml(deviceTableHtml)); - } - - @Override - public Loader<Cursor> onCreateLoader(int id, Bundle args) { - return null; - } - - @Override - public void onLoadFinished(Loader<Cursor> loader, Cursor data) { - } - - @Override - public void onLoaderReset(Loader<Cursor> loader) { - } -} \ No newline at end of file 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 d1249934460aebef1968d8ab832c1dcdbb04f5f3..6538ae565c03771d99a6f3eaf7b144dbb0c16c81 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MainActivity.java @@ -6,11 +6,13 @@ import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.net.Uri; @@ -23,9 +25,9 @@ import android.preference.PreferenceManager; import android.provider.Settings; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; -import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.NotificationCompat; +import android.support.v7.view.menu.ActionMenuItemView; import android.support.v7.widget.Toolbar; import android.text.Html; import android.text.format.DateUtils; @@ -36,12 +38,16 @@ import android.view.MenuItem; import android.view.View; import android.widget.TextView; import android.widget.TextView.BufferType; - -import com.github.mikephil.charting.charts.LineChart; -import com.github.mikephil.charting.data.LineData; -import com.github.mikephil.charting.data.realm.implementation.RealmLineDataSet; -import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; -import com.github.mikephil.charting.utils.ColorTemplate; +import android.widget.Toast; + +import com.jjoe64.graphview.DefaultLabelFormatter; +import com.jjoe64.graphview.GraphView; +import com.jjoe64.graphview.Viewport; +import com.jjoe64.graphview.series.DataPoint; +import com.jjoe64.graphview.series.DataPointInterface; +import com.jjoe64.graphview.series.OnDataPointTapListener; +import com.jjoe64.graphview.series.PointsGraphSeries; +import com.jjoe64.graphview.series.Series; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.materialdrawer.AccountHeaderBuilder; import com.mikepenz.materialdrawer.Drawer; @@ -51,7 +57,8 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import java.text.DateFormat; import java.text.DecimalFormat; -import java.util.ArrayList; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Queue; @@ -61,28 +68,45 @@ import info.nightscout.android.R; import info.nightscout.android.USB.UsbHidDriver; 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.MedtronicCnlAlarmReceiver; import info.nightscout.android.medtronic.service.MedtronicCnlIntentService; -import info.nightscout.android.model.medtronicNg.ContourNextLinkInfo; import info.nightscout.android.model.medtronicNg.PumpInfo; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; import info.nightscout.android.settings.SettingsActivity; import info.nightscout.android.upload.nightscout.NightscoutUploadIntentService; import io.realm.Realm; +import io.realm.RealmChangeListener; import io.realm.RealmResults; import io.realm.Sort; import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper; public class MainActivity extends AppCompatActivity implements OnSharedPreferenceChangeListener, OnEulaAgreedTo { private static final String TAG = MainActivity.class.getSimpleName(); + public static final float MMOLXLFACTOR = 18.016f; public static int batLevel = 0; + public static boolean reducePollOnPumpAway = false; + public static long pollInterval = MedtronicCnlIntentService.POLL_PERIOD_MS; + public static long lowBatteryPollInterval = MedtronicCnlIntentService.LOW_BATTERY_POLL_PERIOD_MS; + private static long activePumpMac; + private int chartZoom = 3; + private boolean hasZoomedChart = false; + + private NumberFormat sgvFormatter; + 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; private PumpInfo mActivePump; private TextView mTextViewLog; // This will eventually move to a status page. - private LineChart mChart; + private GraphView mChart; private Intent mNightscoutUploadService; private Handler mUiRefreshHandler = new Handler(); private Runnable mUiRefreshRunnable = new RefreshDisplayRunnable(); @@ -90,8 +114,48 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc private StatusMessageReceiver statusMessageReceiver = new StatusMessageReceiver(); private MedtronicCnlAlarmReceiver medtronicCnlAlarmReceiver = new MedtronicCnlAlarmReceiver(); - public static void setActivePumpMac(long pumpMac) { - activePumpMac = pumpMac; + /** + * calculate the next poll timestamp based on last svg event + * + * @param pumpStatusData + * @return timestamp + */ + public static long getNextPoll(PumpStatusEvent pumpStatusData) { + long nextPoll = pumpStatusData.getEventDate().getTime() + pumpStatusData.getPumpTimeOffset(), + now = System.currentTimeMillis(); + + // 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) / MainActivity.pollInterval)) * MainActivity.pollInterval + + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS; + if (pumpStatusData.getBatteryPercentage() > 25) { + // poll every 5 min + nextPoll += MainActivity.pollInterval; + } else { + // if pump battery seems to be empty reduce polling to save battery (every 15 min) + //TODO add message & document it + nextPoll += MainActivity.lowBatteryPollInterval; + } + } + + 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 @@ -111,13 +175,30 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc stopCgmService(); } + // setup preferences + MainActivity.pollInterval = Long.parseLong(prefs.getString("pollInterval", Long.toString(MedtronicCnlIntentService.POLL_PERIOD_MS))); + MainActivity.lowBatteryPollInterval = Long.parseLong(prefs.getString("lowBatPollInterval", Long.toString(MedtronicCnlIntentService.LOW_BATTERY_POLL_PERIOD_MS))); + MainActivity.reducePollOnPumpAway = prefs.getBoolean("doublePollOnPumpAway", false); + chartZoom = Integer.parseInt(prefs.getString("chartZoom", "3")); + mmolxl = prefs.getBoolean("mmolxl", false); + mmolxlDecimals = prefs.getBoolean("mmolDecimals", false); + + if (mmolxl) { + if (mmolxlDecimals) + sgvFormatter = new DecimalFormat("0.00"); + else + sgvFormatter = new DecimalFormat("0.0"); + } else { + sgvFormatter = new DecimalFormat("0"); + } + // Disable battery optimization to avoid missing values on 6.0+ // taken from https://github.com/NightscoutFoundation/xDrip/blob/master/app/src/main/java/com/eveningoutpost/dexdrip/Home.java#L277L298 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final String packageName = getPackageName(); - //Log.d(TAG, "Maybe ignoring battery optimization"); final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (!pm.isIgnoringBatteryOptimizations(packageName)) { Log.d(TAG, "Requesting ignore battery optimization"); try { @@ -137,8 +218,8 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc statusMessageReceiver, new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_STATUS_MESSAGE)); LocalBroadcastManager.getInstance(this).registerReceiver( - new RefreshDataReceiver(), - new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_REFRESH_DATA)); + new UpdatePumpReceiver(), + new IntentFilter(MedtronicCnlIntentService.Constants.ACTION_UPDATE_PUMP)); mEnableCgmService = Eula.show(this, prefs); @@ -221,7 +302,8 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc stopCgmService(); finish(); } else if (drawerItem.equals(itemGetNow)) { - startCgmService(); + // It was triggered by user so start reading of data now and not based on last poll. + startCgmService(0); } else if (drawerItem.equals(itemClearLog)) { clearLogText(); } @@ -232,7 +314,54 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc .build(); mTextViewLog = (TextView) findViewById(R.id.textview_log); - mChart = (LineChart) findViewById(R.id.chart); + + mChart = (GraphView) findViewById(R.id.chart); + + // disable scrolling at the moment + mChart.getViewport().setScalable(false); + mChart.getViewport().setScrollable(false); + mChart.getViewport().setXAxisBoundsManual(true); + final long now = System.currentTimeMillis(), + left = now - chartZoom * 60 * 60 * 1000; + + mChart.getViewport().setMaxX(now); + mChart.getViewport().setMinX(left); + + mChart.getViewport().setOnXAxisBoundsChangedListener(new Viewport.OnXAxisBoundsChangedListener() { + @Override + public void onXAxisBoundsChanged(double minX, double maxX, Reason reason) { + double rightX = mChart.getSeries().get(0).getHighestValueX(); + hasZoomedChart = (rightX != maxX || rightX - chartZoom * 60 * 60 * 1000 != minX); + } + }); + + mChart.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (!mChart.getSeries().isEmpty() && !mChart.getSeries().get(0).isEmpty()) { + double rightX = mChart.getSeries().get(0).getHighestValueX(); + mChart.getViewport().setMaxX(rightX); + mChart.getViewport().setMinX(rightX - chartZoom * 60 * 60 * 1000); + } + hasZoomedChart = false; + return true; + } + }); + mChart.getGridLabelRenderer().setNumHorizontalLabels(6); + mChart.getGridLabelRenderer().setHumanRounding(false); + + mChart.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() { + 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 { + return sgvFormatter.format(value); + } + }} + ); } @Override @@ -247,7 +376,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)); // setup self handling alarm receiver - medtronicCnlAlarmReceiver.setContext(getBaseContext()); + MedtronicCnlAlarmManager.setContext(getBaseContext()); } @Override @@ -269,24 +398,6 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc return true; } - private boolean hasDetectedCnl() { - if (mRealm.where(ContourNextLinkInfo.class).count() == 0) { - new AlertDialog.Builder(this, R.style.AppTheme) - .setTitle("No registered Contour Next Link devices") - .setMessage("To register a Contour Next Link you must first plug it in, and get a reading from the pump.") - .setCancelable(false) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }) - .setIcon(android.R.drawable.ic_dialog_alert) - .show(); - return false; - } - return true; - } - private boolean hasUsbPermission() { UsbManager usbManager = (UsbManager) this.getSystemService(Context.USB_SERVICE); UsbDevice cnlDevice = UsbHidDriver.getUsbDevice(usbManager, MedtronicCnlIntentService.USB_VID, MedtronicCnlIntentService.USB_PID); @@ -330,7 +441,18 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } private void startCgmService() { - startCgmService(System.currentTimeMillis() + 1000); + startCgmServiceDelayed(0); + } + + private void startCgmServiceDelayed(long delay) { + RealmResults<PumpStatusEvent> results = mRealm.where(PumpStatusEvent.class) + .findAllSorted("eventDate", Sort.DESCENDING); + + if (results.size() > 0) { + startCgmService(getNextPoll(results.first()) + delay); + } else { + startCgmService(System.currentTimeMillis() + (delay==0?1000:delay)); + } } private void startCgmService(long initialPoll) { @@ -340,11 +462,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc return; } - //clearLogText(); - // Cancel any existing polling. stopCgmService(); - medtronicCnlAlarmReceiver.setAlarm(initialPoll); + MedtronicCnlAlarmManager.setAlarm(initialPoll); } private void uploadCgmData() { @@ -353,7 +473,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc private void stopCgmService() { Log.i(TAG, "stopCgmService called"); - medtronicCnlAlarmReceiver.cancelAlarm(); + MedtronicCnlAlarmManager.cancelAlarm(); } private void showDisconnectionNotification(String title, String message) { @@ -413,7 +533,29 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc mEnableCgmService = true; startCgmService(); } - } else if (key.equals("mmolxl")) { + } else if (key.equals("mmolxl") || key.equals("mmolDecimals")) { + mmolxl = sharedPreferences.getBoolean("mmolxl", false); + mmolxlDecimals = sharedPreferences.getBoolean("mmolDecimals", false); + if (mmolxl) { + if (mmolxlDecimals) + sgvFormatter = new DecimalFormat("0.00"); + else + sgvFormatter = new DecimalFormat("0.0"); + } else { + sgvFormatter = new DecimalFormat("0"); + } + refreshDisplay(); + } else if (key.equals("pollInterval")) { + MainActivity.pollInterval = Long.parseLong(sharedPreferences.getString("pollInterval", + Long.toString(MedtronicCnlIntentService.POLL_PERIOD_MS))); + } else if (key.equals("lowBatPollInterval")) { + MainActivity.lowBatteryPollInterval = Long.parseLong(sharedPreferences.getString("lowBatPollInterval", + Long.toString(MedtronicCnlIntentService.LOW_BATTERY_POLL_PERIOD_MS))); + } else if (key.equals("doublePollOnPumpAway")) { + MainActivity.reducePollOnPumpAway = sharedPreferences.getBoolean("doublePollOnPumpAway", false); + } else if (key.equals("chartZoom")) { + chartZoom = Integer.parseInt(sharedPreferences.getString("chartZoom", "3")); + hasZoomedChart = false; refreshDisplay(); } } @@ -434,10 +576,8 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } public void openUsbRegistration() { - if (hasDetectedCnl()) { - Intent loginIntent = new Intent(this, GetHmacAndKeyActivity.class); - startActivity(loginIntent); - } + Intent manageCNLIntent = new Intent(this, ManageCNLActivity.class); + startActivity(manageCNLIntent); } private String renderTrendHtml(PumpStatusEvent.CGM_TREND trend) { @@ -461,9 +601,17 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } } + public static void setActivePumpMac(long pumpMac) { + activePumpMac = pumpMac; + } + private PumpInfo getActivePump() { if (activePumpMac != 0L && (mActivePump == null || !mActivePump.isValid() || mActivePump.getPumpMac() != activePumpMac)) { - mActivePump = null; + if (mActivePump != null) { + // remove listener on old pump + mActivePump.removeChangeListeners(); + mActivePump = null; + } PumpInfo pump = mRealm .where(PumpInfo.class) @@ -472,6 +620,41 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc if (pump != null && pump.isValid()) { 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(); + + // 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("eventDate", 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(); + } + }); + } + + // TODO - handle isOffline in NightscoutUploadIntentService? + refreshDisplay(); + } + }); } } @@ -513,7 +696,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) { @@ -521,7 +704,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)); @@ -553,7 +736,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc 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 (prefs.getBoolean("mmolxl", false)) { + if (mmolxl) { textViewUnits.setText(R.string.text_unit_mmolxl); } else { textViewUnits.setText(R.string.text_unit_mgxdl); @@ -566,122 +749,181 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc PumpInfo pump = getActivePump(); - if (pump != null && pump.isValid()) { + if (pump != null && pump.isValid() && pump.getPumpHistory().size() > 0) { pumpStatusData = pump.getPumpHistory().last(); } // FIXME - grab the last item from the activePump's getPumpHistory - RealmResults<PumpStatusEvent> results = - mRealm.where(PumpStatusEvent.class) - .findAllSorted("eventDate", Sort.ASCENDING); + updateChart(mRealm.where(PumpStatusEvent.class) + .greaterThan("eventDate", new Date(System.currentTimeMillis() - 1000*60*60*24)) + .findAllSorted("eventDate", Sort.ASCENDING)); - if (pumpStatusData == null) { - return; - } + if (pumpStatusData != null) { - DecimalFormat df; - if (prefs.getBoolean("mmolDecimals", false)) - df = new DecimalFormat("0.00"); - else - df = new DecimalFormat("0.0"); + String sgvString; + if (mmolxl) { + float fBgValue = (float) pumpStatusData.getSgv(); + sgvString = sgvFormatter.format(fBgValue / MMOLXLFACTOR); + Log.d(TAG, sgvString + " mmol/L"); + } else { + sgvString = String.valueOf(pumpStatusData.getSgv()); + Log.d(TAG, sgvString + " mg/dL"); + } - String sgvString, units; - if (prefs.getBoolean("mmolxl", false)) { - float fBgValue = (float) pumpStatusData.getSgv(); - sgvString = df.format(fBgValue / 18.016f); - units = "mmol/L"; - Log.d(TAG, "mmolxl true --> " + sgvString); + textViewBg.setText(sgvString); + textViewBgTime.setText(DateUtils.getRelativeTimeSpanString(pumpStatusData.getEventDate().getTime())); + textViewTrend.setText(Html.fromHtml(renderTrendHtml(pumpStatusData.getCgmTrend()))); + textViewIOB.setText(String.format(Locale.getDefault(), "%.2f", pumpStatusData.getActiveInsulin())); + + ActionMenuItemView batIcon = ((ActionMenuItemView) findViewById(R.id.status_battery)); + if (batIcon != null) { + switch (pumpStatusData.getBatteryPercentage()) { + case 0: + batIcon.setTitle("0%"); + batIcon.setIcon(getResources().getDrawable(R.drawable.battery_0)); + break; + case 25: + batIcon.setTitle("25%"); + batIcon.setIcon(getResources().getDrawable(R.drawable.battery_25)); + break; + case 50: + batIcon.setTitle("50%"); + batIcon.setIcon(getResources().getDrawable(R.drawable.battery_50)); + break; + case 75: + batIcon.setTitle("75%"); + batIcon.setIcon(getResources().getDrawable(R.drawable.battery_75)); + break; + case 100: + batIcon.setTitle("100%"); + batIcon.setIcon(getResources().getDrawable(R.drawable.battery_100)); + break; + default: + batIcon.setTitle(getResources().getString(R.string.menu_name_status)); + batIcon.setIcon(getResources().getDrawable(R.drawable.battery_unknown)); + } + } - } else { - sgvString = String.valueOf(pumpStatusData.getSgv()); - units = "mg/dL"; - Log.d(TAG, "mmolxl false --> " + sgvString); } - textViewBg.setText(sgvString); - textViewUnits.setText(units); - textViewBgTime.setText(DateUtils.getRelativeTimeSpanString(pumpStatusData.getEventDate().getTime())); - textViewTrend.setText(Html.fromHtml(renderTrendHtml(pumpStatusData.getCgmTrend()))); - textViewIOB.setText(String.format(Locale.getDefault(), "%.2f", pumpStatusData.getActiveInsulin())); - - // TODO - waiting for MPAndroidCharts 3.0.0. This will fix: - // Date support - // Realm v1.0.0 support - //updateChart(results); - // Run myself again in 60 seconds; mUiRefreshHandler.postDelayed(this, 60000L); } private void updateChart(RealmResults<PumpStatusEvent> results) { - RealmLineDataSet<PumpStatusEvent> lineDataSet = new RealmLineDataSet<>(results, "eventDate", "sgv"); - - lineDataSet.setDrawCircleHole(false); - lineDataSet.setColor(ColorTemplate.rgb("#FF5722")); - lineDataSet.setCircleColor(ColorTemplate.rgb("#FF5722")); - lineDataSet.setLineWidth(1.8f); - lineDataSet.setCircleSize(3.6f); - - ArrayList<ILineDataSet> dataSets = new ArrayList<ILineDataSet>(); - dataSets.add(lineDataSet); - LineData lineData = new LineData(dataSets); + mChart.getGridLabelRenderer().setNumHorizontalLabels(6); - // set data - mChart.setMinimumHeight(200); - mChart.setData(lineData); - } - } + int size = results.size(); + if (size == 0) { + final long now = System.currentTimeMillis(), + left = now - chartZoom * 60 * 60 * 1000; - private class RefreshDataReceiver extends BroadcastReceiver { + mChart.getViewport().setXAxisBoundsManual(true); + mChart.getViewport().setMaxX(now); + mChart.getViewport().setMinX(left); - @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()) { + mChart.getViewport().setYAxisBoundsManual(true); + if (mmolxl) { + mChart.getViewport().setMinY(80 / MMOLXLFACTOR); + mChart.getViewport().setMaxY(120 / MMOLXLFACTOR); + } else { + mChart.getViewport().setMinY(80); + mChart.getViewport().setMaxY(120); + } + mChart.postInvalidate(); return; } - PumpStatusEvent pumpStatusData = null; + DataPoint[] entries = new DataPoint[size]; + final long left = System.currentTimeMillis() - chartZoom * 60 * 60 * 1000; - PumpInfo pump = getActivePump(); + int pos = 0; + for (PumpStatusEvent pumpStatus: results) { + // turn your data into Entry objects + int sgv = pumpStatus.getSgv(); - if (pump != null && pump.isValid()) { - pumpStatusData = pump.getPumpHistory().last(); - } else { - return; + if (mmolxl) { + entries[pos++] = new DataPoint(pumpStatus.getEventDate(), (float) pumpStatus.getSgv() / MMOLXLFACTOR); + } else { + entries[pos++] = new DataPoint(pumpStatus.getEventDate(), (float) pumpStatus.getSgv()); + } } - long nextPoll = pumpStatusData.getEventDate().getTime() + pumpStatusData.getPumpTimeOffset() - + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS + MedtronicCnlIntentService.POLL_PERIOD_MS; - startCgmService(nextPoll); - - // 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("eventDate", new Date(System.currentTimeMillis() - (24 * 60 * 60 * 1000))) - .findAll(); - - if (results.size() > 0) { - mRealm.executeTransaction(new Realm.Transaction() { + if (mChart.getSeries().size() == 0) { +// long now = System.currentTimeMillis(); +// entries = new DataPoint[1000]; +// int j = 0; +// for(long i = now - 24*60*60*1000; i < now - 30*60*1000; i+= 5*60*1000) { +// entries[j++] = new DataPoint(i, (float) (Math.random()*200 + 89)); +// } +// entries = Arrays.copyOfRange(entries, 0, j); + + PointsGraphSeries sgvSerie = new PointsGraphSeries(entries); +// sgvSerie.setSize(3.6f); +// sgvSerie.setColor(Color.LTGRAY); + + + sgvSerie.setOnDataPointTapListener(new OnDataPointTapListener() { + DateFormat mFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM); + @Override - public void execute(Realm realm) { - // Delete all matches - Log.d(TAG, "Deleting " + results.size() + " records from realm"); - results.deleteAllFromRealm(); + public void onTap(Series series, DataPointInterface dataPoint) { + double sgv = dataPoint.getY(); + + StringBuilder sb = new StringBuilder(mFormat.format(new Date((long) dataPoint.getX())) + ": "); + sb.append(sgvFormatter.format(sgv)); + Toast.makeText(getBaseContext(), sb.toString(), Toast.LENGTH_SHORT).show(); } }); + + sgvSerie.setCustomShape(new PointsGraphSeries.CustomShape() { + @Override + public void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint) { + double sgv = dataPoint.getY(); + if (sgv < (mmolxl?4.5:80)) + paint.setColor(Color.RED); + else if (sgv <= (mmolxl?10:180)) + paint.setColor(Color.GREEN); + else if (sgv <= (mmolxl?14:260)) + paint.setColor(Color.YELLOW); + else + paint.setColor(Color.RED); + canvas.drawCircle(x, y, 3.6f, paint); + } + }); + + mChart.getViewport().setYAxisBoundsManual(false); + mChart.addSeries(sgvSerie); + } else { + if (entries.length > 0) { + ((PointsGraphSeries) mChart.getSeries().get(0)).resetData(entries); + } } - // TODO - handle isOffline in NightscoutUploadIntentService? - uploadCgmData(); + // set vieport to latest SGV + long lastSGVTimestamp = (long) mChart.getSeries().get(0).getHighestValueX(); + if (!hasZoomedChart) { + mChart.getViewport().setMaxX(lastSGVTimestamp); + mChart.getViewport().setMinX(lastSGVTimestamp - chartZoom * 60 * 60 * 1000); + } + } + } + /** + * has to be done in MainActivity thread + */ + private class UpdatePumpReceiver extends BroadcastReceiver { - refreshDisplay(); + @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(); } } @@ -703,7 +945,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc if (hasUsbPermission()) { // Give the USB a little time to warm up first - startCgmService(System.currentTimeMillis() + MedtronicCnlIntentService.USB_WARMUP_TIME_MS); + startCgmServiceDelayed(MedtronicCnlIntentService.USB_WARMUP_TIME_MS); } else { Log.d(TAG, "No permission for USB. Waiting."); waitForUsbPermission(); @@ -732,4 +974,5 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } } } + } diff --git a/app/src/main/java/info/nightscout/android/medtronic/ManageCNLActivity.java b/app/src/main/java/info/nightscout/android/medtronic/ManageCNLActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..b995c0d92f014672185beb3e0f4812da02682aaa --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/ManageCNLActivity.java @@ -0,0 +1,156 @@ +package info.nightscout.android.medtronic; + +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.IconicsDrawable; + +import java.util.ArrayList; + +import info.nightscout.android.R; +import info.nightscout.android.model.medtronicNg.ContourNextLinkInfo; +import io.realm.Realm; +import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper; + +/** + * A login screen that offers login via username/password. + */ +public class ManageCNLActivity extends AppCompatActivity { + private Realm mRealm; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_manage_cnl); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + + if (toolbar != null) { + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeAsUpIndicator( + new IconicsDrawable(this) + .icon(GoogleMaterial.Icon.gmd_arrow_back) + .color(Color.WHITE) + .sizeDp(24) + ); + getSupportActionBar().setElevation(0); + getSupportActionBar().setTitle("Manage your devices"); + } + + mRealm = Realm.getDefaultInstance(); + + //generate list + ArrayList<ContourNextLinkInfo> list = new ArrayList<ContourNextLinkInfo>(); + + list.addAll(mRealm.where(ContourNextLinkInfo.class).findAll()); + + //instantiate custom adapter + CNLAdapter adapter = new CNLAdapter(list, this); + + //handle listview and assign adapter + ListView lView = (ListView) findViewById(R.id.cnl_list); + lView.setAdapter(adapter); + lView.addHeaderView(getLayoutInflater().inflate(R.layout.manage_cnl_listview_header, null)); + lView.setEmptyView(findViewById(R.id.manage_cnl_listview_empty)); //getLayoutInflater().inflate(R.layout.manage_cnl_listview_empty, null)); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // avoid memory leaks + mRealm = null; + finish(); + break; + } + return true; + } + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)); + } + + private class CNLAdapter extends BaseAdapter implements ListAdapter { + private ArrayList<ContourNextLinkInfo> list = new ArrayList<>(); + private Context context; + + public CNLAdapter(ArrayList<ContourNextLinkInfo> list, Context context) { + this.list = list; + this.context = context; + } + + @Override + public int getCount() { + return list.size(); + } + + @Override + public Object getItem(int pos) { + return list.get(pos); + } + + @Override + public long getItemId(int pos) { + return pos; //list.get(pos).getSerialNumber(); + //just return 0 if your list items do not have an Id variable. + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.cnl_item, null); + } + + //Handle TextView and display string from your list + TextView listItemText = (TextView) view.findViewById(R.id.cnl_mac); + listItemText.setText(list.get(position).getSerialNumber()); + + //Handle buttons and add onClickListeners + Button deleteBtn = (Button) view.findViewById(R.id.delete_btn); + + deleteBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // deleting CNL form database + mRealm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + ContourNextLinkInfo cnlToDelete = Realm.getDefaultInstance().where(ContourNextLinkInfo.class).equalTo("serialNumber", list.get(position).getSerialNumber()).findFirst(); + cnlToDelete.deleteFromRealm(); + list.remove(position); + notifyDataSetChanged(); + } + }); + + } + }); + + return view; + } + } +} \ No newline at end of file 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 6dd908f0dd3f57b8d8c585d51690eaee4b82df5b..6763c82d683bcd7daf32ba61f95294216a4a2a70 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlReader.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlReader.java @@ -4,87 +4,58 @@ import android.util.Log; import org.apache.commons.lang3.ArrayUtils; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.math.BigDecimal; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; 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 java.util.regex.Matcher; -import java.util.regex.Pattern; import info.nightscout.android.USB.UsbHidDriver; import info.nightscout.android.medtronic.message.BeginEHSMMessage; -import info.nightscout.android.medtronic.message.ChannelNegotiateMessage; -import info.nightscout.android.medtronic.message.ChecksumException; -import info.nightscout.android.medtronic.message.ContourNextLinkBinaryMessage; +import info.nightscout.android.medtronic.message.ChannelNegotiateRequestMessage; +import info.nightscout.android.medtronic.message.ChannelNegotiateResponseMessage; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.message.CloseConnectionRequestMessage; import info.nightscout.android.medtronic.message.ContourNextLinkCommandMessage; -import info.nightscout.android.medtronic.message.ContourNextLinkMessage; -import info.nightscout.android.medtronic.message.ContourNextLinkMessageHandler; -import info.nightscout.android.medtronic.message.EncryptionException; +import info.nightscout.android.medtronic.message.DeviceInfoRequestCommandMessage; +import info.nightscout.android.medtronic.message.DeviceInfoResponseCommandMessage; +import info.nightscout.android.medtronic.exception.EncryptionException; import info.nightscout.android.medtronic.message.EndEHSMMessage; -import info.nightscout.android.medtronic.message.MedtronicMessage; -import info.nightscout.android.medtronic.message.MessageUtils; +import info.nightscout.android.medtronic.message.OpenConnectionRequestMessage; import info.nightscout.android.medtronic.message.PumpBasalPatternRequestMessage; import info.nightscout.android.medtronic.message.PumpBasalPatternResponseMessage; import info.nightscout.android.medtronic.message.PumpStatusRequestMessage; import info.nightscout.android.medtronic.message.PumpStatusResponseMessage; import info.nightscout.android.medtronic.message.PumpTimeRequestMessage; import info.nightscout.android.medtronic.message.PumpTimeResponseMessage; +import info.nightscout.android.medtronic.message.ReadHistoryInfoRequestMessage; +import info.nightscout.android.medtronic.message.ReadHistoryInfoResponseMessage; +import info.nightscout.android.medtronic.message.ReadInfoRequestMessage; import info.nightscout.android.medtronic.message.ReadInfoResponseMessage; +import info.nightscout.android.medtronic.message.RequestLinkKeyRequestMessage; import info.nightscout.android.medtronic.message.RequestLinkKeyResponseMessage; -import info.nightscout.android.medtronic.message.UnexpectedMessageException; +import info.nightscout.android.medtronic.exception.UnexpectedMessageException; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; -import info.nightscout.android.utils.HexDump; /** * Created by lgoedhart on 24/03/2016. */ -public class MedtronicCnlReader implements ContourNextLinkMessageHandler { - +public class MedtronicCnlReader { private static final String TAG = MedtronicCnlReader.class.getSimpleName(); - private static final int USB_BLOCKSIZE = 64; - private static final int READ_TIMEOUT_MS = 5000; - private static final String BAYER_USB_HEADER = "ABC"; - private static final byte[] RADIO_CHANNELS = {0x14, 0x11, 0x0e, 0x17, 0x1a}; private UsbHidDriver mDevice; private MedtronicCnlSession mPumpSession = new MedtronicCnlSession(); - private String mStickSerial = null; + private static final int SLEEP_MS = 500; + public MedtronicCnlReader(UsbHidDriver device) { mDevice = device; } - private static PumpStatusEvent.CGM_TREND fromMessageByte(byte messageByte) { - switch (messageByte) { - case (byte) 0x60: - return PumpStatusEvent.CGM_TREND.FLAT; - case (byte) 0xc0: - return PumpStatusEvent.CGM_TREND.DOUBLE_UP; - case (byte) 0xa0: - return PumpStatusEvent.CGM_TREND.SINGLE_UP; - case (byte) 0x80: - return PumpStatusEvent.CGM_TREND.FOURTY_FIVE_UP; - case (byte) 0x40: - return PumpStatusEvent.CGM_TREND.FOURTY_FIVE_DOWN; - case (byte) 0x20: - return PumpStatusEvent.CGM_TREND.SINGLE_DOWN; - case (byte) 0x00: - return PumpStatusEvent.CGM_TREND.DOUBLE_DOWN; - default: - return PumpStatusEvent.CGM_TREND.NOT_COMPUTABLE; - } - } - public String getStickSerial() { return mStickSerial; } @@ -93,198 +64,75 @@ public class MedtronicCnlReader implements ContourNextLinkMessageHandler { return mPumpSession; } - public byte[] readMessage() throws IOException, TimeoutException { - ByteArrayOutputStream responseMessage = new ByteArrayOutputStream(); - - byte[] responseBuffer = new byte[USB_BLOCKSIZE]; - int bytesRead; - int messageSize = 0; - - do { - bytesRead = mDevice.read(responseBuffer, READ_TIMEOUT_MS); - - if (bytesRead == -1) { - throw new TimeoutException("Timeout waiting for response from pump"); - } else if (bytesRead > 0) { - // Validate the header - ByteBuffer header = ByteBuffer.allocate(3); - header.put(responseBuffer, 0, 3); - String headerString = new String(header.array()); - if (!headerString.equals(BAYER_USB_HEADER)) { - throw new IOException("Unexpected header received"); - } - messageSize = responseBuffer[3]; - responseMessage.write(responseBuffer, 4, messageSize); - } else { - Log.w(TAG, "readMessage: got a zero-sized response."); - } - } while (bytesRead > 0 && messageSize == 60); - - String responseString = HexDump.dumpHexString(responseMessage.toByteArray()); - Log.d(TAG, "READ: " + responseString); - - return responseMessage.toByteArray(); - } - - @Override - public void sendMessage(ContourNextLinkMessage message) throws IOException { - sendMessage(message.encode()); - if (message instanceof ContourNextLinkBinaryMessage) { - mPumpSession.incrBayerSequenceNumber(); - } + public void requestDeviceInfo() + throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { + DeviceInfoResponseCommandMessage response = new DeviceInfoRequestCommandMessage().send(mDevice); - if (message instanceof MedtronicMessage) { - mPumpSession.incrMedtronicSequenceNumber(); - } + //TODO - extract more details form the device info. + mStickSerial = response.getSerial(); } - @Override - public ContourNextLinkMessage receiveMessage() { - return null; - } + public void enterControlMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { + boolean doRetry; - public void sendMessage(byte[] message) throws IOException { - - int pos = 0; - - while (message.length > pos) { - ByteBuffer outputBuffer = ByteBuffer.allocate(USB_BLOCKSIZE); - int sendLength = (pos + 60 > message.length) ? message.length - pos : 60; - outputBuffer.put(BAYER_USB_HEADER.getBytes()); - outputBuffer.put((byte) sendLength); - outputBuffer.put(message, pos, sendLength); - - mDevice.write(outputBuffer.array(), 200); - pos += sendLength; - - String outputString = HexDump.dumpHexString(outputBuffer.array()); - Log.d(TAG, "WRITE: " + outputString); - } - } - - // TODO - get rid of this - it should be in a message decoder - private void checkControlMessage(byte[] msg, byte controlCharacter) throws IOException, TimeoutException, UnexpectedMessageException { - if (msg.length != 1 || msg[0] != controlCharacter) { - throw new UnexpectedMessageException(String.format(Locale.getDefault(), "Expected to get control character '%d' Got '%d'.", - (int) controlCharacter, (int) msg[0])); - } - } - - public void requestDeviceInfo() throws IOException, TimeoutException, UnexpectedMessageException { - new ContourNextLinkCommandMessage("X").send(this); - - boolean doRetry = false; - - // TODO - parse this into an ASTM record for the device info. - try { - // The stick will return either the ASTM message, or the ENQ first. The order can change, - // so we need to handle both cases - byte[] response1 = readMessage(); - byte[] response2 = readMessage(); - - if (response1[0] == ASCII.EOT.value) { - // response 1 is the ASTM message - checkControlMessage(response2, ASCII.ENQ.value); - extractStickSerial(new String(response1)); - } else { - // response 2 is the ASTM message - checkControlMessage(response1, ASCII.ENQ.value); - extractStickSerial(new String(response2)); - } - } catch (TimeoutException e) { - // Terminate comms with the pump, then try again - new ContourNextLinkCommandMessage(ASCII.EOT.value).send(this); - doRetry = true; - } finally { - if (doRetry) { - requestDeviceInfo(); - } - } - } - - private void extractStickSerial(String astmMessage) { - Pattern pattern = Pattern.compile(".*?\\^(\\d{4}-\\d{7})\\^.*"); - Matcher matcher = pattern.matcher(astmMessage); - if (matcher.find()) { - mStickSerial = matcher.group(1); - } - } - - public void enterControlMode() throws IOException, TimeoutException, UnexpectedMessageException { - boolean doRetry = false; - - try { - new ContourNextLinkCommandMessage(ASCII.NAK.value).send(this); - checkControlMessage(readMessage(), ASCII.EOT.value); - new ContourNextLinkCommandMessage(ASCII.ENQ.value).send(this); - checkControlMessage(readMessage(), ASCII.ACK.value); - } catch (UnexpectedMessageException e2) { - // Terminate comms with the pump, then try again - new ContourNextLinkCommandMessage(ASCII.EOT.value).send(this); - doRetry = true; - } finally { - if (doRetry) { - enterControlMode(); + do { + doRetry = false; + try { + new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.NAK) + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.EOT); + new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.ENQ) + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + } catch (UnexpectedMessageException e2) { + try { + new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT).send(mDevice); + } catch (IOException e) {} + finally { + doRetry = true; + } } - } + } while (doRetry); } - public void enterPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException { + public void enterPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { Log.d(TAG, "Begin enterPasshtroughMode"); - new ContourNextLinkCommandMessage("W|").send(this); - checkControlMessage(readMessage(), ASCII.ACK.value); - new ContourNextLinkCommandMessage("Q|").send(this); - checkControlMessage(readMessage(), ASCII.ACK.value); - new ContourNextLinkCommandMessage("1|").send(this); - checkControlMessage(readMessage(), ASCII.ACK.value); + new ContourNextLinkCommandMessage("W|") + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + new ContourNextLinkCommandMessage("Q|") + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + new ContourNextLinkCommandMessage("1|") + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); Log.d(TAG, "Finished enterPasshtroughMode"); } - public void openConnection() throws IOException, TimeoutException, NoSuchAlgorithmException { + public void openConnection() throws IOException, TimeoutException, NoSuchAlgorithmException, ChecksumException, EncryptionException, UnexpectedMessageException { Log.d(TAG, "Begin openConnection"); - new ContourNextLinkBinaryMessage(ContourNextLinkBinaryMessage.CommandType.OPEN_CONNECTION, mPumpSession, mPumpSession.getHMAC()).send(this); - // FIXME - We need to care what the response message is - wrong MAC and all that - readMessage(); + new OpenConnectionRequestMessage(mPumpSession, mPumpSession.getHMAC()).send(mDevice); Log.d(TAG, "Finished openConnection"); } - public void requestReadInfo() throws IOException, TimeoutException, EncryptionException, ChecksumException { + public void requestReadInfo() throws IOException, TimeoutException, EncryptionException, ChecksumException, UnexpectedMessageException { Log.d(TAG, "Begin requestReadInfo"); - new ContourNextLinkBinaryMessage(ContourNextLinkBinaryMessage.CommandType.READ_INFO, mPumpSession, null).send(this); - - ContourNextLinkMessage response = ReadInfoResponseMessage.fromBytes(mPumpSession, readMessage()); + ReadInfoResponseMessage response = new ReadInfoRequestMessage(mPumpSession).send(mDevice); - // FIXME - this needs to go into ReadInfoResponseMessage - ByteBuffer infoBuffer = ByteBuffer.allocate(16); - infoBuffer.order(ByteOrder.BIG_ENDIAN); - infoBuffer.put(response.encode(), 0x21, 16); - long linkMAC = infoBuffer.getLong(0); - long pumpMAC = infoBuffer.getLong(8); + long linkMAC = response.getLinkMAC(); + long pumpMAC = response.getPumpMAC(); this.getPumpSession().setLinkMAC(linkMAC); this.getPumpSession().setPumpMAC(pumpMAC); - Log.d(TAG, String.format("Finished requestReadInfo. linkMAC = '%d', pumpMAC = '%d", linkMAC, pumpMAC)); + Log.d(TAG, String.format("Finished requestReadInfo. linkMAC = '%s', pumpMAC = '%s'", + Long.toHexString(linkMAC), Long.toHexString(pumpMAC))); } - public void requestLinkKey() throws IOException, TimeoutException, EncryptionException, ChecksumException { + public void requestLinkKey() throws IOException, TimeoutException, EncryptionException, ChecksumException, UnexpectedMessageException { Log.d(TAG, "Begin requestLinkKey"); - new ContourNextLinkBinaryMessage(ContourNextLinkBinaryMessage.CommandType.REQUEST_LINK_KEY, mPumpSession, null).send(this); - ContourNextLinkMessage response = RequestLinkKeyResponseMessage.fromBytes(mPumpSession, readMessage()); - - // FIXME - this needs to go into RequestLinkKeyResponseMessage - ByteBuffer infoBuffer = ByteBuffer.allocate(55); - infoBuffer.order(ByteOrder.BIG_ENDIAN); - infoBuffer.put(response.encode(), 0x21, 55); - - byte[] packedLinkKey = infoBuffer.array(); - - this.getPumpSession().setPackedLinkKey(packedLinkKey); + RequestLinkKeyResponseMessage response = new RequestLinkKeyRequestMessage(mPumpSession).send(mDevice); + this.getPumpSession().setKey(response.getKey()); Log.d(TAG, String.format("Finished requestLinkKey. linkKey = '%s'", this.getPumpSession().getKey())); } - public byte negotiateChannel(byte lastRadioChannel) throws IOException, ChecksumException, TimeoutException { + public byte negotiateChannel(byte lastRadioChannel) throws IOException, ChecksumException, TimeoutException, EncryptionException { ArrayList<Byte> radioChannels = new ArrayList<>(Arrays.asList(ArrayUtils.toObject(RADIO_CHANNELS))); if (lastRadioChannel != 0x00) { @@ -300,26 +148,14 @@ public class MedtronicCnlReader implements ContourNextLinkMessageHandler { for (byte channel : radioChannels) { Log.d(TAG, String.format("negotiateChannel: trying channel '%d'...", channel)); mPumpSession.setRadioChannel(channel); - new ChannelNegotiateMessage(mPumpSession).send(this); - - // Don't care what the 0x81 response message is at this stage - Log.d(TAG, "negotiateChannel: Reading 0x81 message"); - readMessage(); - // The 0x80 message - Log.d(TAG, "negotiateChannel: Reading 0x80 message"); - ContourNextLinkMessage response = ContourNextLinkBinaryMessage.fromBytes(readMessage()); - byte[] responseBytes = response.encode(); - - Log.d(TAG, "negotiateChannel: Check response length"); - if (responseBytes.length > 46) { - // Looks promising, let's check the last byte of the payload to make sure - if (responseBytes[76] == mPumpSession.getRadioChannel()) { - break; - } else { - throw new IOException(String.format(Locale.getDefault(), "Expected to get a message for channel %d. Got %d", mPumpSession.getRadioChannel(), responseBytes[76])); - } + ChannelNegotiateResponseMessage response = new ChannelNegotiateRequestMessage(mPumpSession).send(mDevice); + + if (response.getRadioChannel() == mPumpSession.getRadioChannel()) { + mPumpSession.setRadioRSSI(response.getRadioRSSI()); + break; } else { - mPumpSession.setRadioChannel((byte) 0); + mPumpSession.setRadioChannel((byte)0); + mPumpSession.setRadioRSSI((byte)0); } } @@ -327,216 +163,101 @@ public class MedtronicCnlReader implements ContourNextLinkMessageHandler { return mPumpSession.getRadioChannel(); } - public void beginEHSMSession() throws EncryptionException, IOException, TimeoutException { + public void beginEHSMSession() throws EncryptionException, IOException, TimeoutException, ChecksumException, UnexpectedMessageException { Log.d(TAG, "Begin beginEHSMSession"); - new BeginEHSMMessage(mPumpSession).send(this); - // The Begin EHSM Session only has an 0x81 response - readMessage(); + new BeginEHSMMessage(mPumpSession).send(mDevice); Log.d(TAG, "Finished beginEHSMSession"); } - public Date getPumpTime() throws EncryptionException, IOException, ChecksumException, TimeoutException { + 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) - - new PumpTimeRequestMessage(mPumpSession).send(this); - // Read the 0x81 - readMessage(); - // Read the 0x80 - ContourNextLinkMessage response = PumpTimeResponseMessage.fromBytes(mPumpSession, readMessage()); - - if (response.encode().length < (61 + 8)) { - // Invalid message. Return an invalid date. - // TODO - deal with this more elegantly - Log.e(TAG, "Invalid message received for getPumpTime"); - return new Date(); + // 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); } - // FIXME - this needs to go into PumpTimeResponseMessage - ByteBuffer dateBuffer = ByteBuffer.allocate(8); - dateBuffer.order(ByteOrder.BIG_ENDIAN); - dateBuffer.put(response.encode(), 0x3d, 8); - long rtc = dateBuffer.getInt(0) & 0x00000000ffffffffL; - long offset = dateBuffer.getInt(4); + PumpTimeResponseMessage response = new PumpTimeRequestMessage(mPumpSession).send(mDevice); - Log.d(TAG, "Finished getPumpTime with date " + MessageUtils.decodeDateTime(rtc, offset)); - return MessageUtils.decodeDateTime(rtc, offset); + Log.d(TAG, "Finished getPumpTime with date " + response.getPumpTime()); + return response.getPumpTime(); } - public void getPumpStatus(PumpStatusEvent pumpRecord, long pumpTimeOffset) throws IOException, EncryptionException, ChecksumException, TimeoutException { - Log.d(TAG, "Begin getPumpStatus"); - // FIXME - throw if not in EHSM mode (add a state machine) - - new PumpStatusRequestMessage(mPumpSession).send(this); - // Read the 0x81 - readMessage(); - - // Read the 0x80 - ContourNextLinkMessage response = PumpStatusResponseMessage.fromBytes(mPumpSession, readMessage()); - - if (response.encode().length < (57 + 96)) { - // Invalid message. Don't try and parse it - // TODO - deal with this more elegantly - Log.e(TAG, "Invalid message received for getPumpStatus"); - return; - } + public PumpStatusEvent updatePumpStatus(PumpStatusEvent pumpRecord) throws IOException, EncryptionException, ChecksumException, TimeoutException, UnexpectedMessageException { + Log.d(TAG, "Begin updatePumpStatus"); - // FIXME - this needs to go into PumpStatusResponseMessage - ByteBuffer statusBuffer = ByteBuffer.allocate(96); - statusBuffer.order(ByteOrder.BIG_ENDIAN); - statusBuffer.put(response.encode(), 0x39, 96); - - // Status Flags - pumpRecord.setSuspended((statusBuffer.get(0x03) & 0x01) != 0x00); - pumpRecord.setBolusing((statusBuffer.get(0x03) & 0x02) != 0x00); - pumpRecord.setDeliveringInsulin((statusBuffer.get(0x03) & 0x10) != 0x00); - pumpRecord.setTempBasalActive((statusBuffer.get(0x03) & 0x20) != 0x00); - pumpRecord.setCgmActive((statusBuffer.get(0x03) & 0x40) != 0x00); - - // Active basal pattern - pumpRecord.setActiveBasalPattern(statusBuffer.get(0x1a)); - - // Normal basal rate - long rawNormalBasal = statusBuffer.getInt(0x1b); - pumpRecord.setBasalRate(new BigDecimal(rawNormalBasal / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue()); - - // Temp basal rate - // TODO - need to figure this one out - //long rawTempBasal = statusBuffer.getShort(0x21) & 0x0000ffff; - //pumpRecord.setTempBasalRate(new BigDecimal(rawTempBasal / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue()); - - // Temp basal percentage - pumpRecord.setTempBasalPercentage(statusBuffer.get(0x23)); - - // Temp basal minutes remaining - pumpRecord.setTempBasalMinutesRemaining((short) (statusBuffer.getShort(0x24) & 0x0000ffff)); - - // Units of insulin delivered as basal today - // TODO - is this basal? Do we have a total Units delivered elsewhere? - pumpRecord.setBasalUnitsDeliveredToday(statusBuffer.getInt(0x26)); - - // Pump battery percentage - pumpRecord.setBatteryPercentage((statusBuffer.get(0x2a))); - - // Reservoir amount - long rawReservoirAmount = statusBuffer.getInt(0x2b); - pumpRecord.setReservoirAmount(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 insulinMinutes = statusBuffer.get(0x30); - pumpRecord.setMinutesOfInsulinRemaining((short) ((insulinHours * 60) + insulinMinutes)); - - // Active insulin - long rawActiveInsulin = statusBuffer.getShort(0x33) & 0x0000ffff; - pumpRecord.setActiveInsulin(new BigDecimal(rawActiveInsulin / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue()); - - // CGM SGV - pumpRecord.setSgv(statusBuffer.getShort(0x35) & 0x0000ffff); // In mg/DL. 0 means no CGM reading - - // SGV Date - long rtc; - long offset; - if ((pumpRecord.getSgv() & 0x200) == 0x200) { - // Sensor error. Let's reset. FIXME - solve this more elegantly later - pumpRecord.setSgv(0); - rtc = 0; - offset = 0; - pumpRecord.setCgmTrend(PumpStatusEvent.CGM_TREND.NOT_SET); - } else { - rtc = statusBuffer.getInt(0x37) & 0x00000000ffffffffL; - offset = statusBuffer.getInt(0x3b); - pumpRecord.setCgmTrend(fromMessageByte(statusBuffer.get(0x40))); + // 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); } - // TODO - this should go in the sgvDate, and eventDate should be the time of this poll. - pumpRecord.setEventDate(new Date(MessageUtils.decodeDateTime(rtc, offset).getTime() - pumpTimeOffset)); - // Predictive low suspend - // TODO - there is more status info in this byte other than just a boolean yes/no - pumpRecord.setLowSuspendActive(statusBuffer.get(0x3f) != 0); + PumpStatusResponseMessage response = new PumpStatusRequestMessage(mPumpSession).send(mDevice); + response.updatePumpRecord(pumpRecord); - // Recent Bolus Wizard BGL - pumpRecord.setRecentBolusWizard(statusBuffer.get(0x48) != 0); - pumpRecord.setBolusWizardBGL(statusBuffer.getShort(0x49) & 0x0000ffff); // In mg/DL + Log.d(TAG, "Finished updatePumpStatus"); - Log.d(TAG, "Finished getPumpStatus"); + return pumpRecord; } - public void getBasalPatterns() throws EncryptionException, IOException, ChecksumException, TimeoutException { + public void getBasalPatterns() throws EncryptionException, IOException, ChecksumException, TimeoutException, UnexpectedMessageException { Log.d(TAG, "Begin getBasalPatterns"); // FIXME - throw if not in EHSM mode (add a state machine) - new PumpBasalPatternRequestMessage(mPumpSession).send(this); - // Read the 0x81 - readMessage(); + PumpBasalPatternResponseMessage response = new PumpBasalPatternRequestMessage(mPumpSession).send(mDevice); - // Read the 0x80 - ContourNextLinkMessage response = PumpBasalPatternResponseMessage.fromBytes(mPumpSession, readMessage()); + Log.d(TAG, "Finished getBasalPatterns"); + } - // TODO - determine message validity - /* - if (response.encode().length < (61 + 8)) { - // Invalid message. - // TODO - deal with this more elegantly - Log.e(TAG, "Invalid message received for getBasalPatterns"); - return; - } - */ - // FIXME - this needs to go into PumpBasalPatternResponseMessage - ByteBuffer basalRatesBuffer = ByteBuffer.allocate(96); - basalRatesBuffer.order(ByteOrder.BIG_ENDIAN); - basalRatesBuffer.put(response.encode(), 0x39, 96); + public void getHistory() throws EncryptionException, IOException, ChecksumException, TimeoutException, UnexpectedMessageException { + Log.d(TAG, "Begin getHistory"); + // FIXME - throw if not in EHSM mode (add a state machine) - Log.d(TAG, "Finished getBasalPatterns"); + ReadHistoryInfoResponseMessage response = new ReadHistoryInfoRequestMessage(mPumpSession).send(mDevice); + + Log.d(TAG, "Finished getHistory"); } - public void endEHSMSession() throws EncryptionException, IOException, TimeoutException { + public void endEHSMSession() throws EncryptionException, IOException, TimeoutException, ChecksumException, UnexpectedMessageException { Log.d(TAG, "Begin endEHSMSession"); - new EndEHSMMessage(mPumpSession).send(this); - // The End EHSM Session only has an 0x81 response - readMessage(); + new EndEHSMMessage(mPumpSession).send(mDevice); Log.d(TAG, "Finished endEHSMSession"); } - public void closeConnection() throws IOException, TimeoutException { + public void closeConnection() throws IOException, TimeoutException, ChecksumException, EncryptionException, NoSuchAlgorithmException, UnexpectedMessageException { Log.d(TAG, "Begin closeConnection"); - new ContourNextLinkBinaryMessage(ContourNextLinkBinaryMessage.CommandType.CLOSE_CONNECTION, mPumpSession, null).send(this); - // FIXME - We need to care what the response message is - wrong MAC and all that - readMessage(); + new CloseConnectionRequestMessage(mPumpSession, mPumpSession.getHMAC()).send(mDevice); Log.d(TAG, "Finished closeConnection"); } - public void endPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException { + public void endPassthroughMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { Log.d(TAG, "Begin endPassthroughMode"); - new ContourNextLinkCommandMessage("W|").send(this); - checkControlMessage(readMessage(), ASCII.ACK.value); - new ContourNextLinkCommandMessage("Q|").send(this); - checkControlMessage(readMessage(), ASCII.ACK.value); - new ContourNextLinkCommandMessage("0|").send(this); - checkControlMessage(readMessage(), ASCII.ACK.value); + new ContourNextLinkCommandMessage("W|") + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + new ContourNextLinkCommandMessage("Q|") + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); + new ContourNextLinkCommandMessage("0|") + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ACK); Log.d(TAG, "Finished endPassthroughMode"); } - public void endControlMode() throws IOException, TimeoutException, UnexpectedMessageException { + public void endControlMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { Log.d(TAG, "Begin endControlMode"); - new ContourNextLinkCommandMessage(ASCII.EOT.value).send(this); - checkControlMessage(readMessage(), ASCII.ENQ.value); + new ContourNextLinkCommandMessage(ContourNextLinkCommandMessage.ASCII.EOT) + .send(mDevice, SLEEP_MS).checkControlMessage(ContourNextLinkCommandMessage.ASCII.ENQ); Log.d(TAG, "Finished endControlMode"); } - - public enum ASCII { - STX(0x02), - EOT(0x04), - ENQ(0x05), - ACK(0x06), - NAK(0x15); - - private byte value; - - ASCII(int code) { - this.value = (byte) code; - } - } } 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 215e7f3e6174163da35db8824c928d87f5d454bb..a86820f7b72797aa1a2f7c1c19dc5fb6034db430 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlSession.java +++ b/app/src/main/java/info/nightscout/android/medtronic/MedtronicCnlSession.java @@ -11,7 +11,6 @@ import java.security.NoSuchAlgorithmException; public class MedtronicCnlSession { private static final String HMAC_PADDING = "A4BD6CED9A42602564F413123"; - private byte[] HMAC; private byte[] key; private String stickSerial; @@ -20,13 +19,11 @@ public class MedtronicCnlSession { private long pumpMAC; private byte radioChannel; + private byte radioRSSI; + private int bayerSequenceNumber = 1; private int medtronicSequenceNumber = 1; - /*public byte[] getHMAC() { - return HMAC; - }*/ - public byte[] getHMAC() throws NoSuchAlgorithmException { String shortSerial = this.stickSerial.replaceAll("\\d+-", ""); byte[] message = (shortSerial + HMAC_PADDING).getBytes(); @@ -80,6 +77,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,34 +97,14 @@ public class MedtronicCnlSession { this.radioChannel = radioChannel; } - public void setHMAC(byte[] hmac) { - this.HMAC = hmac; + public void setRadioRSSI(byte radioRSSI) { + this.radioRSSI = radioRSSI; } public void setKey(byte[] key) { this.key = key; } - public void setPackedLinkKey(byte[] packedLinkKey) { - this.key = new byte[16]; - - int pos = this.stickSerial.charAt(this.stickSerial.length() - 1) & 7; - - for (int i = 0; i < this.key.length; i++) { - if ((packedLinkKey[pos + 1] & 1) == 1) { - this.key[i] = (byte) ~packedLinkKey[pos]; - } else { - this.key[i] = packedLinkKey[pos]; - } - - if (((packedLinkKey[pos + 1] >> 1) & 1) == 0) { - pos += 3; - } else { - pos += 2; - } - } - } - public String getStickSerial() { return stickSerial; } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ChecksumException.java b/app/src/main/java/info/nightscout/android/medtronic/exception/ChecksumException.java similarity index 77% rename from app/src/main/java/info/nightscout/android/medtronic/message/ChecksumException.java rename to app/src/main/java/info/nightscout/android/medtronic/exception/ChecksumException.java index 1cdfe5341906933a69ece4385c4ea6d91c64bc1a..25418f212f5ddae2f52e3613eb5263f80821e49e 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ChecksumException.java +++ b/app/src/main/java/info/nightscout/android/medtronic/exception/ChecksumException.java @@ -1,4 +1,4 @@ -package info.nightscout.android.medtronic.message; +package info.nightscout.android.medtronic.exception; /** * Created by lgoedhart on 26/03/2016. diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/EncryptionException.java b/app/src/main/java/info/nightscout/android/medtronic/exception/EncryptionException.java similarity index 77% rename from app/src/main/java/info/nightscout/android/medtronic/message/EncryptionException.java rename to app/src/main/java/info/nightscout/android/medtronic/exception/EncryptionException.java index a86847431c501032a8ed3c9c27bb1087bf0ee288..ccc1cb726fc7605f7b0a946329624101c41b2a4b 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/EncryptionException.java +++ b/app/src/main/java/info/nightscout/android/medtronic/exception/EncryptionException.java @@ -1,4 +1,4 @@ -package info.nightscout.android.medtronic.message; +package info.nightscout.android.medtronic.exception; /** * Created by lgoedhart on 26/03/2016. diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/UnexpectedMessageException.java b/app/src/main/java/info/nightscout/android/medtronic/exception/UnexpectedMessageException.java similarity index 78% rename from app/src/main/java/info/nightscout/android/medtronic/message/UnexpectedMessageException.java rename to app/src/main/java/info/nightscout/android/medtronic/exception/UnexpectedMessageException.java index 71ec46969f14e8caf316b27ed30c3666144df880..4d2daeb207cfed052e0dc76c14b57fed7fb4a701 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/UnexpectedMessageException.java +++ b/app/src/main/java/info/nightscout/android/medtronic/exception/UnexpectedMessageException.java @@ -1,4 +1,4 @@ -package info.nightscout.android.medtronic.message; +package info.nightscout.android.medtronic.exception; /** * Created by lgoedhart on 26/03/2016. diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/BeginEHSMMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/BeginEHSMMessage.java index e2ee643a1a0d9f7de0a0c566c6d2d147ad6e3e19..3902beb03e6e87cdb322871d28fddb1ab80f3a85 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/BeginEHSMMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/BeginEHSMMessage.java @@ -1,12 +1,18 @@ package info.nightscout.android.medtronic.message; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; /** * Created by lgoedhart on 26/03/2016. */ -public class BeginEHSMMessage extends MedtronicSendMessage { - public BeginEHSMMessage(MedtronicCnlSession pumpSession) throws EncryptionException { +public class BeginEHSMMessage extends EHSMMessage { + public BeginEHSMMessage(MedtronicCnlSession pumpSession) throws EncryptionException, ChecksumException { super(SendMessageType.BEGIN_EHSM_SESSION, pumpSession, buildPayload()); } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateMessage.java deleted file mode 100644 index eda3ce6f6f514f3045396216dc658b65e825e5fc..0000000000000000000000000000000000000000 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateMessage.java +++ /dev/null @@ -1,30 +0,0 @@ -package info.nightscout.android.medtronic.message; - -import info.nightscout.android.medtronic.MedtronicCnlSession; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Created by lgoedhart on 26/03/2016. - */ -public class ChannelNegotiateMessage extends MedtronicMessage { - public ChannelNegotiateMessage(MedtronicCnlSession pumpSession) { - super(CommandType.SEND_MESSAGE, CommandAction.CHANNEL_NEGOTIATE, pumpSession, buildPayload(pumpSession)); - } - - protected static byte[] buildPayload( MedtronicCnlSession pumpSession ) { - ByteBuffer payload = ByteBuffer.allocate(26); - payload.order(ByteOrder.LITTLE_ENDIAN); - // The MedtronicMessage sequence number is always sent as 1 for this message, - // even though the sequence should keep incrementing as normal - payload.put((byte) 1); - payload.put(pumpSession.getRadioChannel()); - byte[] unknownBytes = {0, 0, 0, 0x07, 0x07, 0, 0, 0x02}; - payload.put(unknownBytes); - payload.putLong(pumpSession.getLinkMAC()); - payload.putLong(pumpSession.getPumpMAC()); - - return payload.array(); - } -} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateRequestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..84dc029782c0306a1f86e70c19b467fc3e7bea11 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateRequestMessage.java @@ -0,0 +1,58 @@ +package info.nightscout.android.medtronic.message; + +import android.util.Log; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by lgoedhart on 26/03/2016. + */ +public class ChannelNegotiateRequestMessage extends MedtronicRequestMessage<ChannelNegotiateResponseMessage> { + private static final String TAG = ChannelNegotiateRequestMessage.class.getSimpleName(); + + public ChannelNegotiateRequestMessage(MedtronicCnlSession pumpSession) throws ChecksumException { + super(CommandType.SEND_MESSAGE, CommandAction.CHANNEL_NEGOTIATE, pumpSession, buildPayload(pumpSession)); + } + + @Override + public ChannelNegotiateResponseMessage send(UsbHidDriver mDevice) throws IOException, TimeoutException, ChecksumException, EncryptionException { + sendMessage(mDevice); + + // Don't care what the 0x81 response message is at this stage + Log.d(TAG, "negotiateChannel: Reading 0x81 message"); + readMessage(mDevice); + // The 0x80 message + Log.d(TAG, "negotiateChannel: Reading 0x80 message"); + ChannelNegotiateResponseMessage response = this.getResponse(readMessage(mDevice)); + + return response; + } + + @Override + protected ChannelNegotiateResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException { + return new ChannelNegotiateResponseMessage(mPumpSession, payload); + } + + protected static byte[] buildPayload( MedtronicCnlSession pumpSession ) { + ByteBuffer payload = ByteBuffer.allocate(26); + payload.order(ByteOrder.LITTLE_ENDIAN); + // The MedtronicMessage sequence number is always sent as 1 for this message, + // even though the sequence should keep incrementing as normal + payload.put((byte) 1); + payload.put(pumpSession.getRadioChannel()); + byte[] unknownBytes = {0, 0, 0, 0x07, 0x07, 0, 0, 0x02}; + payload.put(unknownBytes); + payload.putLong(pumpSession.getLinkMAC()); + payload.putLong(pumpSession.getPumpMAC()); + + return payload.array(); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..54cff417066ebfb51163b38a485f447d1e55ae64 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateResponseMessage.java @@ -0,0 +1,46 @@ +package info.nightscout.android.medtronic.message; + +import android.util.Log; + +import java.io.IOException; +import java.util.Locale; + +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by lgoedhart on 27/03/2016. + */ +public class ChannelNegotiateResponseMessage extends ContourNextLinkBinaryResponseMessage { + 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); + + byte[] responseBytes = this.encode(); + + 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/CloseConnectionRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/CloseConnectionRequestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..bc656cbd18a4b0c3177e339567f0d5c42d94d74a --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/CloseConnectionRequestMessage.java @@ -0,0 +1,24 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by volker on 10.12.2016. + */ + +public class CloseConnectionRequestMessage extends ContourNextLinkBinaryRequestMessage<CloseConnectionResponseMessage> { + public CloseConnectionRequestMessage(MedtronicCnlSession pumpSession, byte[] payload) throws ChecksumException { + super(CommandType.CLOSE_CONNECTION, pumpSession, payload); + } + + @Override + protected CloseConnectionResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException { + return new CloseConnectionResponseMessage(payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/CloseConnectionResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/CloseConnectionResponseMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..d1d101e925945018f5b3d87b5a2215acf99cbb14 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/CloseConnectionResponseMessage.java @@ -0,0 +1,15 @@ +package info.nightscout.android.medtronic.message; + +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by lgoedhart on 10/05/2016. + */ +public class CloseConnectionResponseMessage extends ContourNextLinkBinaryResponseMessage { + protected CloseConnectionResponseMessage(byte[] payload) throws ChecksumException, EncryptionException { + super(payload); + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryRequestMessage.java similarity index 58% rename from app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryMessage.java rename to app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryRequestMessage.java index 95e94baa56ef73718e3db0d2d13e272f8b48a058..96e72ed8559b0dfbb6d472cc490b7db683dd7a06 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryRequestMessage.java @@ -1,47 +1,49 @@ package info.nightscout.android.medtronic.message; -import info.nightscout.android.medtronic.MedtronicCnlSession; - +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Locale; +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; + /** * Created by lgoedhart on 26/03/2016. */ -public class ContourNextLinkBinaryMessage extends ContourNextLinkMessage{ - //protected ByteBuffer mBayerEnvelope; - //protected ByteBuffer mBayerPayload; - //protected MedtronicCNLSession mPumpSession; - //protected CommandType mCommandType = CommandType.NO_TYPE; - - static int ENVELOPE_SIZE = 33; - - public enum CommandType { - NO_TYPE(0x0), - OPEN_CONNECTION(0x10), - CLOSE_CONNECTION(0x11), - SEND_MESSAGE(0x12), - READ_INFO(0x14), - REQUEST_LINK_KEY(0x16), - SEND_LINK_KEY(0x17), - RECEIVE_MESSAGE(0x80), - SEND_MESSAGE_RESPONSE(0x81), - REQUEST_LINK_KEY_RESPONSE(0x86); - - private byte value; - - CommandType(int commandType) { - value = (byte) commandType; - } +public abstract class ContourNextLinkBinaryRequestMessage<T> extends ContourNextLinkRequestMessage<T> { + private final static int ENVELOPE_SIZE = 33; + + protected CommandType mCommandType = CommandType.NO_TYPE; + protected MedtronicCnlSession mPumpSession; + + public ContourNextLinkBinaryRequestMessage(CommandType commandType, MedtronicCnlSession pumpSession, byte[] payload) throws ChecksumException { + super(buildPayload(commandType, pumpSession, payload)); + + this.mPumpSession = pumpSession; + this.mCommandType = commandType; + + // Validate checksum + // FIXME - this is not needed. Because we're setting the checksum in buildPayload, we know it's + // going to be okay. However, this check does need to be done when reading a message. + byte messageChecksum = this.mPayload.get(32); + byte calculatedChecksum = (byte) (MessageUtils.oneByteSum(this.mPayload.array()) - messageChecksum); - public int getValue() { - return value; + if (messageChecksum != calculatedChecksum) { + throw new ChecksumException(String.format(Locale.getDefault(), "Expected to get %d. Got %d", (int) calculatedChecksum, (int) messageChecksum)); } } - public ContourNextLinkBinaryMessage(CommandType commandType, MedtronicCnlSession pumpSession, byte[] payload) { - super(buildPayload(commandType, pumpSession, payload)); + /** + * Handle incrementing sequence number + * + * @param mDevice + * @throws IOException + */ + protected void sendMessage(UsbHidDriver mDevice) throws IOException { + super.sendMessage(mDevice); + mPumpSession.incrBayerSequenceNumber(); } protected static byte[] buildPayload(CommandType commandType, MedtronicCnlSession pumpSession, byte[] payload) { @@ -55,7 +57,7 @@ public class ContourNextLinkBinaryMessage extends ContourNextLinkMessage{ payloadBuffer.put("000000".getBytes()); // Text of PumpInfo serial, but 000000 for 640g byte[] unknownBytes = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; payloadBuffer.put(unknownBytes); - payloadBuffer.put(commandType.value); + payloadBuffer.put(commandType.getValue()); payloadBuffer.putInt(pumpSession.getBayerSequenceNumber()); byte[] unknownBytes2 = {0, 0, 0, 0, 0}; payloadBuffer.put(unknownBytes2); @@ -73,17 +75,4 @@ public class ContourNextLinkBinaryMessage extends ContourNextLinkMessage{ return payloadBuffer.array(); } - public static ContourNextLinkMessage fromBytes(byte[] bytes) throws ChecksumException { - ContourNextLinkMessage message = new ContourNextLinkMessage(bytes); - - // Validate checksum - byte messageChecksum = message.mPayload.get(32); - byte calculatedChecksum = (byte) (MessageUtils.oneByteSum(message.mPayload.array()) - messageChecksum); - - if (messageChecksum != calculatedChecksum) { - throw new ChecksumException(String.format(Locale.getDefault(), "Expected to get %d. Got %d", (int) calculatedChecksum, (int) messageChecksum)); - } - - return message; - } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryResponseMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..66a9a8b749f5ef47b1a6472f398a1bf9605665dc --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkBinaryResponseMessage.java @@ -0,0 +1,13 @@ +package info.nightscout.android.medtronic.message; + +import info.nightscout.android.medtronic.exception.ChecksumException; + +/** + * Created by lgoedhart on 26/03/2016. + */ +public class ContourNextLinkBinaryResponseMessage extends ContourNextLinkResponseMessage { + + public ContourNextLinkBinaryResponseMessage(byte[] payload) throws ChecksumException { + super(payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkCommandMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkCommandMessage.java index ad114c682a2eeac28d4e1e19a0f42a1131a72c26..36b7530d61104c8c87bd63a8f604c8a6718874fd 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkCommandMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkCommandMessage.java @@ -1,9 +1,21 @@ package info.nightscout.android.medtronic.message; +import java.io.IOException; +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; + /** * Created by lgoedhart on 26/03/2016. */ -public class ContourNextLinkCommandMessage extends ContourNextLinkMessage { +public class ContourNextLinkCommandMessage extends ContourNextLinkRequestMessage<ContourNextLinkCommandResponse> { + public ContourNextLinkCommandMessage(ASCII command) { + super(new byte[]{command.getValue()}); + } + public ContourNextLinkCommandMessage(byte command) { super(new byte[]{command}); } @@ -11,4 +23,10 @@ public class ContourNextLinkCommandMessage extends ContourNextLinkMessage { public ContourNextLinkCommandMessage(String command) { super(command.getBytes()); } + + @Override + protected ContourNextLinkCommandResponse getResponse(byte[] payload) throws ChecksumException { + return new ContourNextLinkCommandResponse(payload); + } + } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkCommandResponse.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkCommandResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..db4839fab75f46b5e6b0277c02782150d0ddaab3 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkCommandResponse.java @@ -0,0 +1,13 @@ +package info.nightscout.android.medtronic.message; + +import info.nightscout.android.medtronic.exception.ChecksumException; + +/** + * Created by volker on 10.12.2016. + */ +public class ContourNextLinkCommandResponse extends ContourNextLinkBinaryResponseMessage { + + public ContourNextLinkCommandResponse(byte[] payload) throws ChecksumException { + super(payload); + } +} 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 2a44cfdeede51c0ec2d21d6b6128aa17a04cbd10..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 @@ -1,32 +1,214 @@ package info.nightscout.android.medtronic.message; +import android.util.Log; + +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MainActivity; +import info.nightscout.android.utils.HexDump; /** * Created by lgoedhart on 26/03/2016. */ -public class ContourNextLinkMessage { +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 = 15000; //ASTM standard is 15 seconds (note was previously set at 10 seconds) + private static final String BAYER_USB_HEADER = "ABC"; + protected ByteBuffer mPayload; - public ContourNextLinkMessage(byte[] bytes) { - if (bytes != null) { - this.mPayload = ByteBuffer.allocate(bytes.length); - this.mPayload.put(bytes); + public enum CommandAction { + NO_TYPE(0x0), + CHANNEL_NEGOTIATE(0x03), + PUMP_REQUEST(0x05), + PUMP_RESPONSE(0x55); + + private byte value; + + CommandAction(int commandAction) { + value = (byte) commandAction; + } + + public byte getValue() { + return value; + } + + public boolean equals(byte value) { + return this.value == value; } } - public byte[] encode() { - return mPayload.array(); + public enum CommandType { + OPEN_CONNECTION(0x10), + CLOSE_CONNECTION(0x11), + SEND_MESSAGE(0x12), + READ_INFO(0x14), + REQUEST_LINK_KEY(0x16), + SEND_LINK_KEY(0x17), + RECEIVE_MESSAGE(0x80), + SEND_MESSAGE_RESPONSE(0x81), + REQUEST_LINK_KEY_RESPONSE(0x86), + + NO_TYPE(0x0); + + private byte value; + + CommandType(int commandType) { + value = (byte) commandType; + } + + public byte getValue() { + return value; + } + + public boolean equals(byte value) { + return this.value == value; + } } - public void send(ContourNextLinkMessageHandler handler) throws IOException { - handler.sendMessage(this); + protected ContourNextLinkMessage(byte[] bytes) { + setPayload(bytes); + } + + public byte[] encode() { + return mPayload.array(); } // FIXME - get rid of this - make a Builder instead protected void setPayload(byte[] payload) { - mPayload = ByteBuffer.allocate(payload.length); - mPayload.put(payload); + if (payload != null) { + mPayload = ByteBuffer.allocate(payload.length); + mPayload.put(payload); + } + } + + protected void sendMessage(UsbHidDriver mDevice) throws IOException { + int pos = 0; + byte[] message = this.encode(); + + while (message.length > pos) { + ByteBuffer outputBuffer = ByteBuffer.allocate(USB_BLOCKSIZE); + int sendLength = (pos + 60 > message.length) ? message.length - pos : 60; + outputBuffer.put(BAYER_USB_HEADER.getBytes()); + outputBuffer.put((byte) sendLength); + outputBuffer.put(message, pos, sendLength); + + mDevice.write(outputBuffer.array(), 200); + pos += sendLength; + + String outputString = HexDump.dumpHexString(outputBuffer.array()); + Log.d(TAG, "WRITE: " + outputString); + } + } + + protected byte[] readMessage(UsbHidDriver mDevice) throws IOException, TimeoutException { + ByteArrayOutputStream responseMessage = new ByteArrayOutputStream(); + + byte[] responseBuffer = new byte[USB_BLOCKSIZE]; + int bytesRead; + int messageSize = 0; + + do { + bytesRead = mDevice.read(responseBuffer, READ_TIMEOUT_MS); + + if (bytesRead == -1) { + throw new TimeoutException("Timeout waiting for response from pump"); + } else if (bytesRead > 0) { + // Validate the header + ByteBuffer header = ByteBuffer.allocate(3); + header.put(responseBuffer, 0, 3); + String headerString = new String(header.array()); + if (!headerString.equals(BAYER_USB_HEADER)) { + throw new IOException("Unexpected header received"); + } + messageSize = responseBuffer[3]; + responseMessage.write(responseBuffer, 4, messageSize); + } else { + Log.w(TAG, "readMessage: got a zero-sized response."); + } + } while (bytesRead > 0 && messageSize == 60); + + String responseString = HexDump.dumpHexString(responseMessage.toByteArray()); + Log.d(TAG, "READ: " + responseString); + + 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), + ENQ(0x05), + ACK(0x06), + NAK(0x15); + + protected byte value; + + ASCII(int code) { + this.value = (byte) code; + } + + public byte getValue() { + return value; + } + + public boolean equals(byte value) { + return this.value == value; + } } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkMessageHandler.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkMessageHandler.java deleted file mode 100644 index 1d669f67b84cd9118e11c91966db05ff95f8738c..0000000000000000000000000000000000000000 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkMessageHandler.java +++ /dev/null @@ -1,11 +0,0 @@ -package info.nightscout.android.medtronic.message; - -import java.io.IOException; - -/** - * Created by lgoedhart on 26/03/2016. - */ -public interface ContourNextLinkMessageHandler { - void sendMessage( ContourNextLinkMessage message ) throws IOException; - ContourNextLinkMessage receiveMessage(); -} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkRequestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..cd25feda28afca655cdb4eb0cc69dad0304e4c58 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkRequestMessage.java @@ -0,0 +1,47 @@ +package info.nightscout.android.medtronic.message; + + +import android.util.Log; + +import java.io.IOException; +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; + +/** + * Created by volker on 12.12.2016. + */ + +public abstract class ContourNextLinkRequestMessage<T> extends ContourNextLinkMessage { + private static final String TAG = ContourNextLinkRequestMessage.class.getSimpleName(); + + protected ContourNextLinkRequestMessage(byte[] bytes) { + super(bytes); + } + + public T send(UsbHidDriver mDevice) throws IOException, TimeoutException, EncryptionException, ChecksumException, UnexpectedMessageException { + return send(mDevice, 0); + } + + public T send(UsbHidDriver mDevice, int millis) throws UnexpectedMessageException, EncryptionException, TimeoutException, ChecksumException, IOException { + sendMessage(mDevice); + if (millis > 0) { + try { + Log.d(TAG, "waiting " + millis +" ms"); + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + + T response = this.getResponse(readMessage(mDevice)); //new ContourNextLinkCommandResponse(); + + // FIXME - We need to care what the response message is - wrong MAC and all that + return response; + } + + protected abstract <T> T getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException, UnexpectedMessageException, TimeoutException; + +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkResponseMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..d20a5613e3345ccdc13a7499852074372490b249 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkResponseMessage.java @@ -0,0 +1,30 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.Locale; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.UnexpectedMessageException; + +/** + * Created by lgoedhart on 26/03/2016. + */ +public abstract class ContourNextLinkResponseMessage extends ContourNextLinkMessage { + + public ContourNextLinkResponseMessage(byte[] payload) throws ChecksumException { + super(payload); + } + + + public void checkControlMessage(ASCII controlCharacter) throws UnexpectedMessageException { + checkControlMessage(mPayload.array(), controlCharacter); + } + + public void checkControlMessage(byte[] msg, ASCII controlCharacter) throws UnexpectedMessageException { + if (msg.length != 1 || !controlCharacter.equals(msg[0])) { + throw new UnexpectedMessageException(String.format(Locale.getDefault(), "Expected to get control character '%d' Got '%d'.", + (int) controlCharacter.getValue(), (int) msg[0])); + } + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/DeviceInfoRequestCommandMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/DeviceInfoRequestCommandMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..38a1bd6b03d1747bf353677df2b956516ca09871 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/DeviceInfoRequestCommandMessage.java @@ -0,0 +1,67 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +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; + +/** + * Created by volker on 10.12.2016. + */ + +public class DeviceInfoRequestCommandMessage extends ContourNextLinkRequestMessage<DeviceInfoResponseCommandMessage> { + public DeviceInfoRequestCommandMessage() { + super("X".getBytes()); + } + + @Override + public DeviceInfoResponseCommandMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, EncryptionException, ChecksumException, UnexpectedMessageException { + sendMessage(mDevice); + + if (millis > 0) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + byte[] response1 = readMessage(mDevice); + if (millis > 0) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + byte[] response2 = readMessage(mDevice); + + boolean doRetry = false; + DeviceInfoResponseCommandMessage response = null; + + do { + try { + if (ASCII.EOT.equals(response1[0])) { + // response 1 is the ASTM message + response = this.getResponse(response1); + // ugly.... + response.checkControlMessage(response2, ASCII.ENQ); + } else { + // response 2 is the ASTM message + response = this.getResponse(response2); + // ugly, too.... + response.checkControlMessage(response1, ASCII.ENQ); + } + } catch (TimeoutException e) { + doRetry = true; + } + } while (doRetry); + + return response; + } + + @Override + protected DeviceInfoResponseCommandMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException, UnexpectedMessageException, TimeoutException { + return new DeviceInfoResponseCommandMessage(payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/DeviceInfoResponseCommandMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/DeviceInfoResponseCommandMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..4147ad8bad97129f068c847228b9a7b79a4ab579 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/DeviceInfoResponseCommandMessage.java @@ -0,0 +1,37 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; +import info.nightscout.android.medtronic.exception.UnexpectedMessageException; + +/** + * Created by lgoedhart on 10/05/2016. + */ +public class DeviceInfoResponseCommandMessage extends ContourNextLinkResponseMessage { + private String serial = ""; + private final Pattern pattern = Pattern.compile(".*?\\^(\\d{4}-\\d{7})\\^.*"); + + protected DeviceInfoResponseCommandMessage(byte[] payload) + throws ChecksumException, EncryptionException, TimeoutException, UnexpectedMessageException, IOException { + super(payload); + + extractStickSerial(new String(payload)); + } + + public String getSerial() { + return serial; + } + + private void extractStickSerial(String astmMessage) { + Matcher matcher = pattern.matcher(astmMessage); + if (matcher.find()) { + serial = matcher.group(1); + } + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..0e064536b650efd76e2ee8dad5867a787671339f --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/EHSMMessage.java @@ -0,0 +1,47 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +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; + +/** + * Created by volker on 22.12.2016. + */ + +public class EHSMMessage extends MedtronicSendMessageRequestMessage<ContourNextLinkResponseMessage>{ + protected EHSMMessage(SendMessageType sendMessageType, MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { + super(sendMessageType, pumpSession, payload); + } + + @Override + public ContourNextLinkResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, UnexpectedMessageException { + + // clear unexpected incoming messages + clearMessage(mDevice); + + sendMessage(mDevice); + if (millis > 0) { + try { + Thread.sleep(millis); + } 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/EndEHSMMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/EndEHSMMessage.java index 0364110375aec3d9c5f6b9c940f500d363758e4f..5335d4cf3b5b48fef9231bc060f011026f5f1f8c 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/EndEHSMMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/EndEHSMMessage.java @@ -1,12 +1,19 @@ package info.nightscout.android.medtronic.message; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; 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; /** * Created by lgoedhart on 26/03/2016. */ -public class EndEHSMMessage extends MedtronicSendMessage { - public EndEHSMMessage(MedtronicCnlSession pumpSession) throws EncryptionException { +public class EndEHSMMessage extends EHSMMessage { + public EndEHSMMessage(MedtronicCnlSession pumpSession) throws EncryptionException, ChecksumException { super(SendMessageType.END_EHSM_SESSION, pumpSession, buildPayload()); } @@ -14,4 +21,5 @@ public class EndEHSMMessage extends MedtronicSendMessage { // Not sure what the payload byte means, but it's the same every time. return new byte[] { 0x01 }; } + } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicPumpMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicPumpMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..6485bd71e6a5d1dd4db717095b5c6999ea4f382a --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicPumpMessage.java @@ -0,0 +1,14 @@ +package info.nightscout.android.medtronic.message; + +import info.nightscout.android.medtronic.MedtronicCnlSession; + +/** + * Created by volker on 15.12.2016. + */ + +public class MedtronicPumpMessage extends ContourNextLinkMessage { + + protected MedtronicPumpMessage(MedtronicCnlSession pumpSession, byte[] bytes) { + super(bytes); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicReceiveMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicReceiveMessage.java deleted file mode 100644 index c2f4a7d58cf10d70925e3d2ab15e3358a4ff3a3f..0000000000000000000000000000000000000000 --- a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicReceiveMessage.java +++ /dev/null @@ -1,67 +0,0 @@ -package info.nightscout.android.medtronic.message; - -import info.nightscout.android.medtronic.MedtronicCnlSession; - -import java.nio.ByteBuffer; - -/** - * Created by lgoedhart on 26/03/2016. - */ -public class MedtronicReceiveMessage extends MedtronicMessage { - static int ENVELOPE_SIZE = 22; - static int ENCRYPTED_ENVELOPE_SIZE = 3; - static int CRC_SIZE = 2; - - protected MedtronicReceiveMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) { - super(commandType, commandAction, pumpSession, payload); - } - - public enum ReceiveMessageType { - NO_TYPE(0x0), - TIME_RESPONSE(0x407); - - private short value; - - ReceiveMessageType(int messageType) { - value = (short) messageType; - } - } - - /** - * MedtronicReceiveMessage: - * +------------------+-----------------+-----------------+---------------------------------+-------------------+--------------------------------+ - * | LE short unknown | LE long pumpMAC | LE long linkMAC | byte[3] responseSequenceNumber? | byte Payload size | byte[] Encrypted Payload bytes | - * +------------------+-----------------+-----------------+---------------------------------+-------------------+--------------------------------+ - * <p/> - * MedtronicReceiveMessage (decrypted payload): - * +----------------------------+-----------------------------+----------------------+--------------------+ - * | byte receiveSequenceNumber | BE short receiveMessageType | byte[] Payload bytes | BE short CCITT CRC | - * +----------------------------+-----------------------------+----------------------+--------------------+ - */ - public static ContourNextLinkMessage fromBytes(MedtronicCnlSession pumpSession, byte[] bytes) throws ChecksumException, EncryptionException { - // TODO - turn this into a factory - ContourNextLinkMessage message = MedtronicMessage.fromBytes(bytes); - - // TODO - Validate the message, inner CCITT, serial numbers, etc - - // If there's not 57 bytes, then we got back a bad message. Not sure how to process these yet. - // Also, READ_INFO and REQUEST_LINK_KEY are not encrypted - if (bytes.length >= 57 && - (bytes[18] != CommandType.READ_INFO.getValue()) && - (bytes[18] != CommandType.REQUEST_LINK_KEY_RESPONSE.getValue())) { - // Replace the encrypted bytes by their decrypted equivalent (same block size) - byte encryptedPayloadSize = bytes[56]; - - ByteBuffer encryptedPayload = ByteBuffer.allocate(encryptedPayloadSize); - encryptedPayload.put(bytes, 57, encryptedPayloadSize); - byte[] decryptedPayload = decrypt(pumpSession.getKey(), pumpSession.getIV(), encryptedPayload.array()); - - // Now that we have the decrypted payload, rewind the mPayload, and overwrite the bytes - // TODO - because this messes up the existing CCITT, do we want to have a separate buffer for the decrypted payload? - // Should be fine provided we check the CCITT first... - message.mPayload.position(57); - message.mPayload.put(decryptedPayload); - } - return message; - } -} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicRequestMessage.java similarity index 61% rename from app/src/main/java/info/nightscout/android/medtronic/message/MedtronicMessage.java rename to app/src/main/java/info/nightscout/android/medtronic/message/MedtronicRequestMessage.java index 25588505ab2e817307d0428108a0e2bb98ef4eb7..9963dd32e133b884e589777d9d2705813df11c5f 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicRequestMessage.java @@ -1,7 +1,6 @@ package info.nightscout.android.medtronic.message; -import info.nightscout.android.medtronic.MedtronicCnlSession; - +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -9,27 +8,19 @@ import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + /** * Created by lgoedhart on 26/03/2016. */ -public class MedtronicMessage extends ContourNextLinkBinaryMessage { +public abstract class MedtronicRequestMessage<T> extends ContourNextLinkBinaryRequestMessage<T> { static int ENVELOPE_SIZE = 2; static int CRC_SIZE = 2; - public enum CommandAction { - NO_TYPE(0x0), - CHANNEL_NEGOTIATE(0x03), - PUMP_REQUEST(0x05), - PUMP_RESPONSE(0x55); - - private byte value; - - CommandAction(int commandAction) { - value = (byte) commandAction; - } - } - - protected MedtronicMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) { + protected MedtronicRequestMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) throws ChecksumException { super(commandType, pumpSession, buildPayload(commandAction, payload)); } @@ -45,7 +36,7 @@ public class MedtronicMessage extends ContourNextLinkBinaryMessage { ByteBuffer payloadBuffer = ByteBuffer.allocate(ENVELOPE_SIZE + payloadLength + CRC_SIZE); payloadBuffer.order(ByteOrder.LITTLE_ENDIAN); - payloadBuffer.put(commandAction.value); + payloadBuffer.put(commandAction.getValue()); payloadBuffer.put((byte) (ENVELOPE_SIZE + payloadLength)); if (payloadLength != 0) { payloadBuffer.put(payload != null ? payload : new byte[0]); @@ -56,12 +47,6 @@ public class MedtronicMessage extends ContourNextLinkBinaryMessage { return payloadBuffer.array(); } - public static ContourNextLinkMessage fromBytes(byte[] bytes) throws ChecksumException { - ContourNextLinkMessage message = ContourNextLinkBinaryMessage.fromBytes(bytes); - - // TODO - Validate the CCITT - return message; - } // TODO - maybe move the SecretKeySpec, IvParameterSpec and Cipher construction into the PumpSession? protected static byte[] encrypt(byte[] key, byte[] iv, byte[] clear) throws EncryptionException { @@ -79,18 +64,8 @@ public class MedtronicMessage extends ContourNextLinkBinaryMessage { return encrypted; } - protected static byte[] decrypt(byte[] key, byte[] iv, byte[] encrypted) throws EncryptionException { - SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - byte[] decrypted; - - try { - Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec); - decrypted = cipher.doFinal(encrypted); - } catch (Exception e ) { - throw new EncryptionException( "Could not decrypt Medtronic Message" ); - } - return decrypted; + protected void sendMessage(UsbHidDriver mDevice) throws IOException { + super.sendMessage(mDevice); + mPumpSession.incrMedtronicSequenceNumber(); } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicResponseMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..47c33dcf6a980e1a93dff4fb3ebefc40ecae3767 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicResponseMessage.java @@ -0,0 +1,127 @@ +package info.nightscout.android.medtronic.message; + +import android.util.Log; + +import java.nio.ByteBuffer; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +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.utils.HexDump; + +/** + * Created by lgoedhart on 26/03/2016. + */ +public class MedtronicResponseMessage extends ContourNextLinkResponseMessage { + private static final String TAG = MedtronicResponseMessage.class.getSimpleName(); + + static int ENVELOPE_SIZE = 22; + static int ENCRYPTED_ENVELOPE_SIZE = 3; + static int CRC_SIZE = 2; + + protected MedtronicCnlSession mPumpSession; + + protected MedtronicResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { + super(payload); + + mPumpSession = pumpSession; + + // TODO - Validate the message, inner CCITT, serial numbers, etc + // If there's not 57 bytes, then we got back a bad message. Not sure how to process these yet. + // Also, READ_INFO and REQUEST_LINK_KEY are not encrypted + if (payload.length >= 57 && + (payload[18] != CommandType.READ_INFO.getValue()) && + (payload[18] != CommandType.REQUEST_LINK_KEY_RESPONSE.getValue())) { + // Replace the encrypted bytes by their decrypted equivalent (same block size) + byte encryptedPayloadSize = payload[56]; + + ByteBuffer encryptedPayload = ByteBuffer.allocate(encryptedPayloadSize); + encryptedPayload.put(payload, 57, encryptedPayloadSize); + byte[] decryptedPayload = decrypt(pumpSession.getKey(), pumpSession.getIV(), encryptedPayload.array()); + + // Now that we have the decrypted payload, rewind the mPayload, and overwrite the bytes + // TODO - because this messes up the existing CCITT, do we want to have a separate buffer for the decrypted payload? + // Should be fine provided we check the CCITT first... + this.mPayload.position(57); + this.mPayload.put(decryptedPayload); + + if (BuildConfig.DEBUG) { + String outputString = HexDump.dumpHexString(this.mPayload.array()); + Log.d(TAG, "DECRYPTED: " + outputString); + } + } + } + + public enum ReceiveMessageType { + NO_TYPE(0x0), + TIME_RESPONSE(0x407); + + private short value; + + ReceiveMessageType(int messageType) { + value = (short) messageType; + } + } + + /** + * MedtronicResponseMessage: + * +------------------+-----------------+-----------------+---------------------------------+-------------------+--------------------------------+ + * | LE short unknown | LE long pumpMAC | LE long linkMAC | byte[3] responseSequenceNumber? | byte Payload size | byte[] Encrypted Payload bytes | + * +------------------+-----------------+-----------------+---------------------------------+-------------------+--------------------------------+ + * <p/> + * MedtronicResponseMessage (decrypted payload): + * +----------------------------+-----------------------------+----------------------+--------------------+ + * | byte receiveSequenceNumber | BE short receiveMessageType | byte[] Payload bytes | BE short CCITT CRC | + * +----------------------------+-----------------------------+----------------------+--------------------+ + */ + public static ContourNextLinkMessage fromBytes(MedtronicCnlSession pumpSession, byte[] bytes) throws ChecksumException, EncryptionException { + // TODO - turn this into a factory + + return new MedtronicResponseMessage(pumpSession, bytes); + /* + ContourNextLinkMessage message = MedtronicMessage.fromBytes(bytes); + + + // TODO - Validate the message, inner CCITT, serial numbers, etc + + // If there's not 57 bytes, then we got back a bad message. Not sure how to process these yet. + // Also, READ_INFO and REQUEST_LINK_KEY are not encrypted + if (bytes.length >= 57 && + (bytes[18] != CommandType.READ_INFO.getValue()) && + (bytes[18] != CommandType.REQUEST_LINK_KEY_RESPONSE.getValue())) { + // Replace the encrypted bytes by their decrypted equivalent (same block size) + byte encryptedPayloadSize = bytes[56]; + + ByteBuffer encryptedPayload = ByteBuffer.allocate(encryptedPayloadSize); + encryptedPayload.put(bytes, 57, encryptedPayloadSize); + byte[] decryptedPayload = decrypt(pumpSession.getKey(), pumpSession.getIV(), encryptedPayload.array()); + + // Now that we have the decrypted payload, rewind the mPayload, and overwrite the bytes + // TODO - because this messes up the existing CCITT, do we want to have a separate buffer for the decrypted payload? + // Should be fine provided we check the CCITT first... + message.mPayload.position(57); + message.mPayload.put(decryptedPayload); + } + return message;*/ + } + + protected static byte[] decrypt(byte[] key, byte[] iv, byte[] encrypted) throws EncryptionException { + SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + byte[] decrypted; + + try { + Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec); + decrypted = cipher.doFinal(encrypted); + } catch (Exception e ) { + throw new EncryptionException( "Could not decrypt Medtronic Message" ); + } + return decrypted; + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessageRequestMessage.java similarity index 73% rename from app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessage.java rename to app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessageRequestMessage.java index 7bba4ded16da96428fd056f5b985ce821ee9e63e..50aee6a666fbba3edb5733bf3e629b17bce98cfe 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessageRequestMessage.java @@ -1,24 +1,28 @@ package info.nightscout.android.medtronic.message; 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 java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; - /** - * Created by lgoedhart on 26/03/2016. + * Created by volker on 18.12.2016. */ -public class MedtronicSendMessage extends MedtronicMessage { + +public abstract class MedtronicSendMessageRequestMessage<T> extends MedtronicRequestMessage<T> { static int ENVELOPE_SIZE = 11; static int ENCRYPTED_ENVELOPE_SIZE = 3; static int CRC_SIZE = 2; public enum SendMessageType { NO_TYPE(0x0), - BEGIN_EHSM_SESSION(0x412), + BEGIN_EHSM_SESSION(0x0412), TIME_REQUEST(0x0403), READ_PUMP_STATUS_REQUEST(0x0112), - READ_BASAL_PATTERN_REQUEST(0x0112), + READ_BASAL_PATTERN_REQUEST(0x0116), END_EHSM_SESSION(0x412); private short value; @@ -28,10 +32,15 @@ public class MedtronicSendMessage extends MedtronicMessage { } } - protected MedtronicSendMessage(SendMessageType sendMessageType, MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException { + protected MedtronicSendMessageRequestMessage(SendMessageType sendMessageType, MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { super(CommandType.SEND_MESSAGE, CommandAction.PUMP_REQUEST, pumpSession, buildPayload(sendMessageType, pumpSession, payload)); } + @Override + protected ContourNextLinkResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException, UnexpectedMessageException { + return null; + } + /** * MedtronicSendMessage: * +-----------------+------------------------------+--------------+-------------------+--------------------------------+ @@ -39,9 +48,9 @@ public class MedtronicSendMessage extends MedtronicMessage { * +-----------------+------------------------------+--------------+-------------------+--------------------------------+ * <p/> * MedtronicSendMessage (decrypted payload): - * +-------------------------+----------------------+----------------------+--------------------+ + * +-------------------------+--------------------------+----------------------+--------------------+ * | byte sendSequenceNumber | BE short sendMessageType | byte[] Payload bytes | BE short CCITT CRC | - * +-------------------------+----------------------+----------------------+--------------------+ + * +-------------------------+--------------------------+----------------------+--------------------+ */ protected static byte[] buildPayload(SendMessageType sendMessageType, MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException { byte payloadLength = (byte) (payload == null ? 0 : payload.length); @@ -69,6 +78,7 @@ public class MedtronicSendMessage extends MedtronicMessage { return payloadBuffer.array(); } + // TODO - This should be dynamically incremented in the Session object protected static byte sendSequenceNumber(SendMessageType sendMessageType) { switch (sendMessageType) { case BEGIN_EHSM_SESSION: diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessageResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessageResponseMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..48820677d9619a21d925c4e795a4ec4b2e195505 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/MedtronicSendMessageResponseMessage.java @@ -0,0 +1,15 @@ +package info.nightscout.android.medtronic.message; + +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by volker on 18.12.2016. + */ + +public class MedtronicSendMessageResponseMessage extends MedtronicResponseMessage { + protected MedtronicSendMessageResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { + super(pumpSession, payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionRequestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..85869867aaf3e5ce37ef1a05fdb76880ca04900b --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionRequestMessage.java @@ -0,0 +1,24 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by volker on 10.12.2016. + */ + +public class OpenConnectionRequestMessage extends ContourNextLinkBinaryRequestMessage<OpenConnectionResponseMessage> { + public OpenConnectionRequestMessage(MedtronicCnlSession pumpSession, byte[] payload) throws ChecksumException { + super(CommandType.OPEN_CONNECTION, pumpSession, payload); + } + + @Override + protected OpenConnectionResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException { + return new OpenConnectionResponseMessage(payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionResponseMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..1f8e6df122f3efffe51a1a71a2f9a02e28e0407b --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionResponseMessage.java @@ -0,0 +1,15 @@ +package info.nightscout.android.medtronic.message; + +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by lgoedhart on 10/05/2016. + */ +public class OpenConnectionResponseMessage extends ContourNextLinkBinaryResponseMessage { + protected OpenConnectionResponseMessage(byte[] payload) throws ChecksumException, EncryptionException { + super(payload); + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternRequestMessage.java index d31eb36a229654efedd6b9849fac749c1b748254..aa71df206607d8ef811415d199576042c063db3b 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternRequestMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternRequestMessage.java @@ -1,12 +1,24 @@ package info.nightscout.android.medtronic.message; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; 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; /** * Created by lgoedhart on 26/03/2016. */ -public class PumpBasalPatternRequestMessage extends MedtronicSendMessage { - public PumpBasalPatternRequestMessage(MedtronicCnlSession pumpSession) throws EncryptionException { +public class PumpBasalPatternRequestMessage extends MedtronicSendMessageRequestMessage<PumpBasalPatternResponseMessage> { + public PumpBasalPatternRequestMessage(MedtronicCnlSession pumpSession) throws EncryptionException, ChecksumException { super(SendMessageType.READ_BASAL_PATTERN_REQUEST, pumpSession, null); } + + @Override + protected PumpBasalPatternResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException, UnexpectedMessageException { + return new PumpBasalPatternResponseMessage(mPumpSession, payload); + } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternResponseMessage.java index d4b4ef97d55422a5d5be2a5179879569a59cfcad..7bc6cf93671f90167dc59210188bc8c96613d621 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternResponseMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/PumpBasalPatternResponseMessage.java @@ -1,21 +1,48 @@ package info.nightscout.android.medtronic.message; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +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.model.medtronicNg.PumpInfo; +import info.nightscout.android.utils.HexDump; /** * Created by lgoedhart on 27/03/2016. */ -public class PumpBasalPatternResponseMessage extends MedtronicReceiveMessage { - protected PumpBasalPatternResponseMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) { - super(commandType, commandAction, pumpSession, payload); - } +public class PumpBasalPatternResponseMessage extends MedtronicSendMessageResponseMessage { + private static final String TAG = PumpBasalPatternResponseMessage.class.getSimpleName(); + + protected PumpBasalPatternResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { + super(pumpSession, payload); - public static ContourNextLinkMessage fromBytes(MedtronicCnlSession pumpSession, byte[] bytes) throws ChecksumException, EncryptionException { - // TODO - turn this into a factory - ContourNextLinkMessage message = MedtronicReceiveMessage.fromBytes(pumpSession, bytes); + // TODO - determine message validity + /* + if (response.encode().length < (61 + 8)) { + // Invalid message. + // TODO - deal with this more elegantly + Log.e(TAG, "Invalid message received for getBasalPatterns"); + return; + } + */ - // TODO - Validate the MessageType - return message; + byte bufferSize = (byte) (this.encode()[0x38] - 2); // TODO - getting the size should be part of the superclass. + ByteBuffer basalBuffer = ByteBuffer.allocate(bufferSize); + basalBuffer.order(ByteOrder.BIG_ENDIAN); + basalBuffer.put(this.encode(), 0x39, bufferSize); + + if (BuildConfig.DEBUG) { + String outputString = HexDump.dumpHexString(basalBuffer.array()); + Log.d(TAG, "BASAL PAYLOAD: " + outputString); + } + } + + public void updateBasalPatterns(PumpInfo pumpInfo) { } } 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 1162bfe139ca65e06fe1eb95571f1ff67520f78c..163f42775f2161bcf92ae7e743c2a0435ba99165 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 @@ -1,12 +1,55 @@ package info.nightscout.android.medtronic.message; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; 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; /** * Created by lgoedhart on 26/03/2016. */ -public class PumpStatusRequestMessage extends MedtronicSendMessage { - public PumpStatusRequestMessage(MedtronicCnlSession pumpSession) throws EncryptionException { +public class PumpStatusRequestMessage extends MedtronicSendMessageRequestMessage<PumpStatusResponseMessage> { + private static final String TAG = PumpStatusRequestMessage.class.getSimpleName(); + + public PumpStatusRequestMessage(MedtronicCnlSession pumpSession) throws EncryptionException, ChecksumException { super(SendMessageType.READ_PUMP_STATUS_REQUEST, pumpSession, null); } + + // TODO - this needs refactoring + public PumpStatusResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException { + sendMessage(mDevice); + if (millis > 0) { + try { + Log.d(TAG, "waiting " + millis +" ms"); + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + // Read the 0x81 + readMessage_0x81(mDevice); + if (millis > 0) { + try { + Log.d(TAG, "waiting " + millis +" ms"); + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + PumpStatusResponseMessage response = this.getResponse(readMessage(mDevice)); + + // clear unexpected incoming messages + clearMessage(mDevice); + + return response; + } + + @Override + protected PumpStatusResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException, UnexpectedMessageException { + return new PumpStatusResponseMessage(mPumpSession, payload); + } } 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 06e9a0f3357d2456f64a5eb7c84ac3218d1e35ee..f97a73934acf42bcb5ffffb6f778a440af158563 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 @@ -1,21 +1,202 @@ package info.nightscout.android.medtronic.message; +import android.util.Log; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Date; +import java.util.Locale; + +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.model.medtronicNg.PumpStatusEvent; +import info.nightscout.android.utils.HexDump; /** * Created by lgoedhart on 27/03/2016. */ -public class PumpStatusResponseMessage extends MedtronicReceiveMessage { - protected PumpStatusResponseMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) { - super(commandType, commandAction, pumpSession, payload); +public class PumpStatusResponseMessage extends MedtronicSendMessageResponseMessage { + private static final String TAG = PumpStatusResponseMessage.class.getSimpleName(); + + // Data from the Medtronic Pump Status message + private boolean suspended; + private boolean bolusing; + private boolean deliveringInsulin; + private boolean tempBasalActive; + private boolean cgmActive; + private byte activeBasalPattern; + private float basalRate; + private float tempBasalRate; + private byte tempBasalPercentage; + private short tempBasalMinutesRemaining; + private float basalUnitsDeliveredToday; + private short batteryPercentage; + private float reservoirAmount; + private short minutesOfInsulinRemaining; // 25h == "more than 1 day" + private float activeInsulin; + private int sgv; + private Date sgvDate; + 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 long rtc; + private long offset; + + protected PumpStatusResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, UnexpectedMessageException { + super(pumpSession, payload); + + if (this.encode().length < (57 + 96)) { + // Invalid message. Don't try and parse it + // TODO - deal with this more elegantly + Log.e(TAG, "Invalid message received for updatePumpStatus"); + throw new UnexpectedMessageException("Invalid message received for updatePumpStatus"); + } + + byte bufferSize = (byte) (this.encode()[0x38] - 2); // TODO - getting the size should be part of the superclass. + ByteBuffer statusBuffer = ByteBuffer.allocate(bufferSize); + statusBuffer.order(ByteOrder.BIG_ENDIAN); + statusBuffer.put(this.encode(), 0x39, bufferSize); + + if (BuildConfig.DEBUG) { + 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; + + // Active basal pattern + activeBasalPattern = statusBuffer.get(0x1a); + + // Normal basal rate + 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; + tempBasalRate = new BigDecimal(rawTempBasal / 10000f).setScale(3, BigDecimal.ROUND_HALF_UP).floatValue(); + + // Temp basal percentage + tempBasalPercentage = statusBuffer.get(0x23); + + // Temp basal minutes remaining + tempBasalMinutesRemaining = (short) (statusBuffer.getShort(0x24) & 0x0000ffff); + + // Units of insulin delivered as basal today + // TODO - is this basal? Do we have a total Units delivered elsewhere? + basalUnitsDeliveredToday = statusBuffer.getInt(0x26); + + // Pump battery percentage + batteryPercentage = statusBuffer.get(0x2a); + + // Reservoir amount + long rawReservoirAmount = statusBuffer.getInt(0x2b); + 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 insulinMinutes = statusBuffer.get(0x30); + minutesOfInsulinRemaining = (short) ((insulinHours * 60) + insulinMinutes); + + // Active insulin + long rawActiveInsulin = statusBuffer.getInt(0x31); + 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 + if ((sgv & 0x200) == 0x200) { + // Sensor error. Let's reset. FIXME - solve this more elegantly later + sgv = 0; + rtc = 0; + offset = 0; + cgmTrend = PumpStatusEvent.CGM_TREND.NOT_SET; + } else { + rtc = statusBuffer.getInt(0x37) & 0x00000000ffffffffL; + offset = statusBuffer.getInt(0x3b); + cgmTrend = PumpStatusEvent.CGM_TREND.fromMessageByte(statusBuffer.get(0x40)); + } + + // SGV Date + // TODO - this should go in the sgvDate, and eventDate should be the time of this poll. + 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; + + // Recent Bolus Wizard BGL + recentBolusWizard = statusBuffer.get(0x48) != 0; + bolusWizardBGL = statusBuffer.getShort(0x49) & 0x0000ffff; // In mg/DL } - public static ContourNextLinkMessage fromBytes(MedtronicCnlSession pumpSession, byte[] bytes) throws ChecksumException, EncryptionException { - // TODO - turn this into a factory - ContourNextLinkMessage message = MedtronicReceiveMessage.fromBytes(pumpSession, bytes); + /** + * update pumpRecord with data read from pump + * + * @param pumpRecord + */ + public void updatePumpRecord(PumpStatusEvent pumpRecord) { + // Status Flags + pumpRecord.setSuspended(suspended); + pumpRecord.setBolusing(bolusing); + pumpRecord.setDeliveringInsulin(deliveringInsulin); + pumpRecord.setTempBasalActive(tempBasalActive); + pumpRecord.setCgmActive(cgmActive); + + // Active basal pattern + pumpRecord.setActiveBasalPattern(activeBasalPattern); + + // Normal basal rate + pumpRecord.setBasalRate(basalRate); + + // Temp basal rate + pumpRecord.setTempBasalRate(tempBasalRate); + + // Temp basal percentage + pumpRecord.setTempBasalPercentage(tempBasalPercentage); + + // Temp basal minutes remaining + pumpRecord.setTempBasalMinutesRemaining(tempBasalMinutesRemaining); + + // Units of insulin delivered as basal today + pumpRecord.setBasalUnitsDeliveredToday(basalUnitsDeliveredToday); + + // Pump battery percentage + pumpRecord.setBatteryPercentage(batteryPercentage); + + // Reservoir amount + pumpRecord.setReservoirAmount(reservoirAmount); + + // Amount of insulin left in pump (in minutes) + pumpRecord.setMinutesOfInsulinRemaining(minutesOfInsulinRemaining); + + // Active insulin + pumpRecord.setActiveInsulin(activeInsulin); + + // CGM SGV + pumpRecord.setSgv(sgv); + pumpRecord.setSgvDate(new Date(sgvDate.getTime() - pumpRecord.getPumpTimeOffset())); + + // SGV Date + pumpRecord.setCgmTrend(cgmTrend); + pumpRecord.setEventDate(new Date(sgvDate.getTime() - pumpRecord.getPumpTimeOffset())); - // TODO - Validate the MessageType + // Predictive low suspend + // TODO - there is more status info in this byte other than just a boolean yes/no + pumpRecord.setLowSuspendActive(lowSuspendActive); - return message; + // Recent Bolus Wizard BGL + pumpRecord.setRecentBolusWizard(recentBolusWizard); + pumpRecord.setBolusWizardBGL(bolusWizardBGL); // In mg/DL } } 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 89305b0a9b3873d3ec4079eeacebf8d740443ffe..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 @@ -1,12 +1,51 @@ package info.nightscout.android.medtronic.message; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; 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; /** * Created by lgoedhart on 26/03/2016. */ -public class PumpTimeRequestMessage extends MedtronicSendMessage { - public PumpTimeRequestMessage(MedtronicCnlSession pumpSession) throws EncryptionException { +public class PumpTimeRequestMessage extends MedtronicSendMessageRequestMessage<PumpTimeResponseMessage> { + public PumpTimeRequestMessage(MedtronicCnlSession pumpSession) throws EncryptionException, ChecksumException { super(SendMessageType.TIME_REQUEST, pumpSession, null); } -} + + @Override + public PumpTimeResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException { + + sendMessage(mDevice); + if (millis > 0) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + // Read the 0x81 + readMessage_0x81(mDevice); + if (millis > 0) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + // 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; + } + + @Override + 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 3437dd69c65e9adfe188966ea14cbc1a6f8afb18..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 @@ -1,21 +1,51 @@ package info.nightscout.android.medtronic.message; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Date; + +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; /** * Created by lgoedhart on 27/03/2016. */ -public class PumpTimeResponseMessage extends MedtronicReceiveMessage { - protected PumpTimeResponseMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) { - super(commandType, commandAction, pumpSession, payload); - } +public class PumpTimeResponseMessage extends MedtronicSendMessageResponseMessage { + private static final String TAG = PumpTimeResponseMessage.class.getSimpleName(); + + private Date pumpTime; - public static ContourNextLinkMessage fromBytes(MedtronicCnlSession pumpSession, byte[] bytes) throws ChecksumException, EncryptionException { - // TODO - turn this into a factory - ContourNextLinkMessage message = MedtronicReceiveMessage.fromBytes(pumpSession, bytes); + protected PumpTimeResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, UnexpectedMessageException { + super(pumpSession, payload); - // TODO - Validate the MessageType + 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"); + throw new UnexpectedMessageException("Invalid message received for getPumpTime"); + } else { + ByteBuffer dateBuffer = ByteBuffer.allocate(8); + dateBuffer.order(ByteOrder.BIG_ENDIAN); + dateBuffer.put(this.encode(), 0x3d, 8); + + if (BuildConfig.DEBUG) { + String outputString = HexDump.dumpHexString(dateBuffer.array()); + Log.d(TAG, "PAYLOAD: " + outputString); + } + + long rtc = dateBuffer.getInt(0) & 0x00000000ffffffffL; + long offset = dateBuffer.getInt(4); + pumpTime = MessageUtils.decodeDateTime(rtc, offset); + } + } - return message; + public Date getPumpTime() { + return pumpTime; } } diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ReadHistoryInfoRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ReadHistoryInfoRequestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..ff7a2f692eee0d339f577e30c8059d84d5421dcf --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ReadHistoryInfoRequestMessage.java @@ -0,0 +1,37 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +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; + +/** + * Created by lgoedhart on 26/03/2016. + */ +public class ReadHistoryInfoRequestMessage extends MedtronicSendMessageRequestMessage<ReadHistoryInfoResponseMessage> { + public ReadHistoryInfoRequestMessage(MedtronicCnlSession pumpSession) throws EncryptionException, ChecksumException { + super(SendMessageType.READ_BASAL_PATTERN_REQUEST, pumpSession, new byte[] { + 2, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + }); + } + + @Override + protected ReadHistoryInfoResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException, UnexpectedMessageException { + return new ReadHistoryInfoResponseMessage(mPumpSession, payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ReadHistoryInfoResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ReadHistoryInfoResponseMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..f77fc57a69cd621854f4712cecb57ab2b7d0b11d --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ReadHistoryInfoResponseMessage.java @@ -0,0 +1,48 @@ +package info.nightscout.android.medtronic.message; + +import android.util.Log; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +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; + +/** + * Created by lgoedhart on 27/03/2016. + */ +public class ReadHistoryInfoResponseMessage extends MedtronicSendMessageResponseMessage { + private static final String TAG = ReadHistoryInfoResponseMessage.class.getSimpleName(); + + protected ReadHistoryInfoResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException, UnexpectedMessageException { + super(pumpSession, payload); + + + if (this.encode().length < 32) { + // Invalid message. + // TODO - deal with this more elegantly + Log.e(TAG, "Invalid message received for ReadHistoryInfo"); + throw new UnexpectedMessageException("Invalid message received for ReadHistoryInfo"); + } else { + + ByteBuffer basalRatesBuffer = ByteBuffer.allocate(payload.length); + basalRatesBuffer.order(ByteOrder.BIG_ENDIAN); + basalRatesBuffer.put(this.encode()); + + if (BuildConfig.DEBUG) { + String outputString = HexDump.dumpHexString(basalRatesBuffer.array()); + Log.d(TAG, "PAYLOAD: " + outputString); + } + String responseString = HexDump.dumpHexString(basalRatesBuffer.array()); + Log.d(TAG, "ReadHistoryInfo: " + responseString); + Log.d(TAG, "ReadHistoryInfo-length: " + basalRatesBuffer.getLong(28)); + } + + + } + +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoRequestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..c357cd5f25ac84be95988ea702987df8ed059deb --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoRequestMessage.java @@ -0,0 +1,24 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by volker on 10.12.2016. + */ + +public class ReadInfoRequestMessage extends ContourNextLinkBinaryRequestMessage<ReadInfoResponseMessage> { + public ReadInfoRequestMessage(MedtronicCnlSession pumpSession) throws ChecksumException { + super(ContourNextLinkBinaryRequestMessage.CommandType.READ_INFO, pumpSession, null); + } + + @Override + protected ReadInfoResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException, IOException { + return new ReadInfoResponseMessage(mPumpSession, payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoResponseMessage.java index 68aab2df7977d4235e1c445b4b8ac0692d16e88b..ad350653318d5caba0c4a8b687fe473173a32ac6 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoResponseMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoResponseMessage.java @@ -1,21 +1,34 @@ package info.nightscout.android.medtronic.message; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; /** * Created by lgoedhart on 10/05/2016. */ -public class ReadInfoResponseMessage extends MedtronicReceiveMessage { - protected ReadInfoResponseMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) { - super(commandType, commandAction, pumpSession, payload); - } +public class ReadInfoResponseMessage extends MedtronicResponseMessage { + private long linkMAC; + private long pumpMAC; - public static ContourNextLinkMessage fromBytes(MedtronicCnlSession pumpSession, byte[] bytes) throws ChecksumException, EncryptionException { - // TODO - turn this into a factory - ContourNextLinkMessage message = MedtronicReceiveMessage.fromBytes(pumpSession, bytes); + protected ReadInfoResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws ChecksumException, EncryptionException { + super(pumpSession, payload); - // TODO - Validate the MessageType + ByteBuffer infoBuffer = ByteBuffer.allocate(16); + infoBuffer.order(ByteOrder.BIG_ENDIAN); + infoBuffer.put(this.encode(), 0x21, 16); + linkMAC = infoBuffer.getLong(0); + pumpMAC = infoBuffer.getLong(8); + } + + public long getLinkMAC() { + return linkMAC; + } - return message; + public long getPumpMAC() { + return pumpMAC; } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyRequestMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyRequestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..bc2d01869d3c7a47d2080d03bd134837d74fc618 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyRequestMessage.java @@ -0,0 +1,24 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import info.nightscout.android.USB.UsbHidDriver; +import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; + +/** + * Created by volker on 10.12.2016. + */ + +public class RequestLinkKeyRequestMessage extends ContourNextLinkBinaryRequestMessage<RequestLinkKeyResponseMessage> { + public RequestLinkKeyRequestMessage(MedtronicCnlSession pumpSession) throws ChecksumException { + super(CommandType.REQUEST_LINK_KEY, pumpSession, null); + } + + @Override + protected RequestLinkKeyResponseMessage getResponse(byte[] payload) throws ChecksumException, EncryptionException { + return new RequestLinkKeyResponseMessage(mPumpSession, payload); + } +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyResponseMessage.java b/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyResponseMessage.java index 423e40fb8ab61296fc9c26995837a56932715a7e..e5bb5ce76b280c6d8dd3a8508653b289feac251f 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyResponseMessage.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyResponseMessage.java @@ -1,21 +1,50 @@ package info.nightscout.android.medtronic.message; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + import info.nightscout.android.medtronic.MedtronicCnlSession; +import info.nightscout.android.medtronic.exception.ChecksumException; +import info.nightscout.android.medtronic.exception.EncryptionException; /** * Created by lgoedhart on 10/05/2016. */ -public class RequestLinkKeyResponseMessage extends MedtronicReceiveMessage { - protected RequestLinkKeyResponseMessage(CommandType commandType, CommandAction commandAction, MedtronicCnlSession pumpSession, byte[] payload) { - super(commandType, commandAction, pumpSession, payload); +public class RequestLinkKeyResponseMessage extends MedtronicResponseMessage { + + private byte[] key; + + protected RequestLinkKeyResponseMessage(MedtronicCnlSession pumpSession, byte[] payload) throws EncryptionException, ChecksumException { + super(pumpSession, payload); + + ByteBuffer infoBuffer = ByteBuffer.allocate(55); + infoBuffer.order(ByteOrder.BIG_ENDIAN); + infoBuffer.put(this.encode(), 0x21, 55); + + setPackedLinkKey(infoBuffer.array()); + } + + public byte[] getKey() { + return key; } - public static ContourNextLinkMessage fromBytes(MedtronicCnlSession pumpSession, byte[] bytes) throws ChecksumException, EncryptionException { - // TODO - turn this into a factory - ContourNextLinkMessage message = MedtronicReceiveMessage.fromBytes(pumpSession, bytes); + private void setPackedLinkKey(byte[] packedLinkKey) { + this.key = new byte[16]; + + int pos = mPumpSession.getStickSerial().charAt(mPumpSession.getStickSerial().length() - 1) & 7; - // TODO - Validate the MessageType + for (int i = 0; i < this.key.length; i++) { + if ((packedLinkKey[pos + 1] & 1) == 1) { + this.key[i] = (byte) ~packedLinkKey[pos]; + } else { + this.key[i] = packedLinkKey[pos]; + } - return message; + if (((packedLinkKey[pos + 1] >> 1) & 1) == 0) { + pos += 3; + } else { + pos += 2; + } + } } } \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..08737107d34864e8164ba6e04f25f4b327ed4c5a --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmManager.java @@ -0,0 +1,95 @@ +package info.nightscout.android.medtronic.service; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.v4.content.WakefulBroadcastReceiver; +import android.util.Log; + +import java.util.Date; + +import info.nightscout.android.medtronic.MainActivity; + +/** + * Created by lgoedhart on 14/07/2016. + */ +public class MedtronicCnlAlarmManager { + private static final String TAG = MedtronicCnlAlarmManager.class.getSimpleName(); + private static final int ALARM_ID = 102; // Alarm id + + private static PendingIntent pendingIntent = null; + private static AlarmManager alarmManager = null; + private static long nextAlarm = Long.MAX_VALUE; + + public static void setContext(Context context) { + cancelAlarm(); + + alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, MedtronicCnlAlarmReceiver.class); + pendingIntent = PendingIntent.getBroadcast(context, ALARM_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + // Setting the alarm in 15 seconds from now + public static void setAlarm() { + setAlarm(System.currentTimeMillis()); + } + + /** + * set the alarm in the future + * + * @param inFuture number of millin in the future + */ + public static void setAlarmAfterMillis(long inFuture) { + setAlarm(System.currentTimeMillis() + inFuture); + } + + // Setting the alarm to call onRecieve + public static void setAlarm(long millis) { + if (alarmManager == null || pendingIntent == null) + return; + + Log.d(TAG, "request to set Alarm at " + new Date(millis)); + + long now = System.currentTimeMillis(); + // don't trigger the past + if (millis < now) + millis = now; + + // only accept alarm nearer than the last one + //if (nextAlarm < millis && nextAlarm > now) { + // return; + //} + + cancelAlarm(); + + nextAlarm = millis; + + Log.d(TAG, "Alarm set to fire at " + new Date(millis)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(millis, null), pendingIntent); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // Android 5.0.0 + 5.0.1 (e.g. Galaxy S4) has a bug. + // Alarms are not exact. Fixed in 5.0.2 oder CM12 + alarmManager.setExact(AlarmManager.RTC_WAKEUP, millis, pendingIntent); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, millis, pendingIntent); + } + } + + // 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); // grace already accounted for when using current intent time to set default restart + } + + // Cancel the alarm. + public static void cancelAlarm() { + if (alarmManager == null || pendingIntent == null) + return; + + alarmManager.cancel(pendingIntent); + } + +} diff --git a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmReceiver.java b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmReceiver.java index 49e648c68cc37df7ca643e632c9c76b1457b32f0..d86076cc3f01cf4a433bb3a9cd29196707bbe0c8 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmReceiver.java +++ b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmReceiver.java @@ -1,15 +1,20 @@ package info.nightscout.android.medtronic.service; +import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Build; +import android.preference.PreferenceManager; import android.support.v4.content.WakefulBroadcastReceiver; import android.util.Log; import java.util.Date; +import info.nightscout.android.medtronic.MainActivity; + /** * Created by lgoedhart on 14/07/2016. */ @@ -17,8 +22,9 @@ public class MedtronicCnlAlarmReceiver extends WakefulBroadcastReceiver { private static final String TAG = MedtronicCnlAlarmReceiver.class.getSimpleName(); private static final int ALARM_ID = 102; // Alarm id - private static PendingIntent pendingIntent = null; - private static AlarmManager alarmManager = null; + public MedtronicCnlAlarmReceiver() { + super(); + } @Override public void onReceive(final Context context, Intent intent) { @@ -26,53 +32,6 @@ public class MedtronicCnlAlarmReceiver extends WakefulBroadcastReceiver { Log.d(TAG, "Received broadcast message at " + new Date(System.currentTimeMillis())); Intent service = new Intent(context, MedtronicCnlIntentService.class); startWakefulService(context, service); - restartAlarm(); - } - - public void setContext(Context context) { - cancelAlarm(); - - alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(context, MedtronicCnlAlarmReceiver.class); - pendingIntent = PendingIntent.getBroadcast(context, ALARM_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - // Setting the alarm in 15 seconds from now - public void setAlarm() { - setAlarm(System.currentTimeMillis()); - } - - // Setting the alarm to call onRecieve - public void setAlarm(long millis) { - if (alarmManager == null || pendingIntent == null) - return; - - cancelAlarm(); - - // don't trigger the past and at least 30 sec away - if (millis < System.currentTimeMillis()) - millis = System.currentTimeMillis(); - - Log.d(TAG, "AlarmManager set to fire at " + new Date(millis)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(millis, null), pendingIntent); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - alarmManager.setExact(AlarmManager.RTC_WAKEUP, millis, pendingIntent); - } else - alarmManager.set(AlarmManager.RTC_WAKEUP, millis, pendingIntent); - } - - // restarting the alarm after MedtronicCnlIntentService.POLL_PERIOD_MS from now - public void restartAlarm() { - setAlarm(System.currentTimeMillis() + MedtronicCnlIntentService.POLL_PERIOD_MS + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS); + MedtronicCnlAlarmManager.restartAlarm(); } - - // Cancel the alarm. - public void cancelAlarm() { - if (alarmManager == null || pendingIntent == null) - return; - - alarmManager.cancel(pendingIntent); - } - } 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 b958a2c3ac004079815a46121313dc9e74cf097a..3d9e0eb7cea5e6e3fbdf9b866e6a1ad59b32ad8e 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,15 +20,17 @@ 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; import info.nightscout.android.medtronic.MainActivity; import info.nightscout.android.medtronic.MedtronicCnlReader; -import info.nightscout.android.medtronic.message.ChecksumException; -import info.nightscout.android.medtronic.message.EncryptionException; +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.message.MessageUtils; -import info.nightscout.android.medtronic.message.UnexpectedMessageException; import info.nightscout.android.model.medtronicNg.ContourNextLinkInfo; import info.nightscout.android.model.medtronicNg.PumpInfo; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; @@ -42,6 +44,7 @@ public class MedtronicCnlIntentService extends IntentService { 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; private static final String TAG = MedtronicCnlIntentService.class.getSimpleName(); @@ -97,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"); @@ -134,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? @@ -151,11 +175,7 @@ public class MedtronicCnlIntentService extends IntentService { .findFirst(); if (info == null) { - // TODO - use realm.createObject()? - info = new ContourNextLinkInfo(); - info.setSerialNumber(cnlReader.getStickSerial()); - - info = realm.copyToRealm(info); + info = realm.createObject(ContourNextLinkInfo.class, cnlReader.getStickSerial()); } cnlReader.getPumpSession().setStickSerial(info.getSerialNumber()); @@ -165,6 +185,7 @@ public class MedtronicCnlIntentService extends IntentService { try { cnlReader.enterPassthroughMode(); cnlReader.openConnection(); + cnlReader.requestReadInfo(); String key = info.getKey(); @@ -180,27 +201,29 @@ public class MedtronicCnlIntentService extends IntentService { long pumpMAC = cnlReader.getPumpSession().getPumpMAC(); Log.i(TAG, "PumpInfo MAC: " + (pumpMAC & 0xffffff)); - MainActivity.setActivePumpMac(pumpMAC); PumpInfo activePump = realm .where(PumpInfo.class) .equalTo("pumpMac", pumpMAC) .findFirst(); if (activePump == null) { - activePump = realm.createObject(PumpInfo.class); - activePump.setPumpMac(pumpMAC); + activePump = realm.createObject(PumpInfo.class, pumpMAC); } + activePump.updateLastQueryTS(); + byte radioChannel = cnlReader.negotiateChannel(activePump.getLastRadioChannel()); 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?"); + 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); String deviceName = String.format("medtronic-640g://%s", cnlReader.getStickSerial()); @@ -216,45 +239,68 @@ public class MedtronicCnlIntentService extends IntentService { // TODO - send ACTION to MainActivity to show offset between pump and uploader. pumpRecord.setPumpTimeOffset(pumpOffset); pumpRecord.setPumpDate(new Date(pumpTime - pumpOffset)); - cnlReader.getPumpStatus(pumpRecord, pumpOffset); - activePump.getPumpHistory().add(pumpRecord); + cnlReader.updatePumpStatus(pumpRecord); - cnlReader.endEHSMSession(); - - boolean cancelTransaction = true; 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(); // There should be the 1 record we've already added in this transaction. - if (checkExistingRecords.size() <= 1) { - realm.commitTransaction(); - cancelTransaction = false; + if (checkExistingRecords.size() == 0) { + activePump.getPumpHistory().add(pumpRecord); } - // Tell the Main Activity we have new data - sendMessage(Constants.ACTION_REFRESH_DATA); + 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 } - if (cancelTransaction) { - realm.cancelTransaction(); - } + 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."); } finally { - //TODO : 05.11.2016 has the close to be here? - cnlReader.closeConnection(); - cnlReader.endPassthroughMode(); - cnlReader.endControlMode(); + try { + cnlReader.closeConnection(); + cnlReader.endPassthroughMode(); + cnlReader.endControlMode(); + } catch (NoSuchAlgorithmException e) {} + } } catch (IOException e) { Log.e(TAG, "Error connecting to Contour Next Link.", e); @@ -279,14 +325,42 @@ 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); } } + private void setActivePumpMac(long pumpMAC) { + MainActivity.setActivePumpMac(pumpMAC); + } + // reliable wake alarm manager wake up for all android versions public static void wakeUpIntent(Context context, long wakeTime, PendingIntent pendingIntent) { final AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); @@ -326,6 +400,7 @@ public class MedtronicCnlIntentService extends IntentService { public static final String ACTION_USB_PERMISSION = "info.nightscout.android.medtronic.USB_PERMISSION"; public static final String ACTION_REFRESH_DATA = "info.nightscout.android.medtronic.service.CGM_DATA"; public static final String ACTION_USB_REGISTER = "info.nightscout.android.medtronic.USB_REGISTER"; + public static final String ACTION_UPDATE_PUMP = "info.nightscout.android.medtronic.UPDATE_PUMP"; public static final String EXTENDED_DATA = "info.nightscout.android.medtronic.service.DATA"; } diff --git a/app/src/main/java/info/nightscout/android/model/medtronicNg/BasalRate.java b/app/src/main/java/info/nightscout/android/model/medtronicNg/BasalRate.java new file mode 100644 index 0000000000000000000000000000000000000000..25c17b251658ddbb585c3cb91179e776be96a039 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/model/medtronicNg/BasalRate.java @@ -0,0 +1,28 @@ +package info.nightscout.android.model.medtronicNg; + +import io.realm.RealmObject; + +/** + * Created by lennart on 22/1/17. + */ + +public class BasalRate extends RealmObject { + private long start; + private float rate; + + public long getStart() { + return start; + } + + public void setStart(long start) { + this.start = start; + } + + public float getRate() { + return rate; + } + + public void setRate(float rate) { + this.rate = rate; + } +} diff --git a/app/src/main/java/info/nightscout/android/model/medtronicNg/BasalSchedule.java b/app/src/main/java/info/nightscout/android/model/medtronicNg/BasalSchedule.java new file mode 100644 index 0000000000000000000000000000000000000000..a151a35de02466504c3d8be7f1b1c6e3e7481ce4 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/model/medtronicNg/BasalSchedule.java @@ -0,0 +1,59 @@ +package info.nightscout.android.model.medtronicNg; + +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.annotations.Index; +import io.realm.annotations.PrimaryKey; + +/** + * Created by lennart on 22/1/17. + */ + +public class BasalSchedule extends RealmObject { + @PrimaryKey + private byte scheduleNumber; + private RealmList<BasalRate> schedule; + + @Index + private boolean uploaded = false; + + public byte getScheduleNumber() { + return scheduleNumber; + } + + public void setScheduleNumber(byte scheduleNumber) { + this.scheduleNumber = scheduleNumber; + } + + public String getName() { + // TODO - internationalise + String[] patternNames = { + "Pattern 1", + "Pattern 2", + "Pattern 3", + "Pattern 4", + "Pattern 5", + "Workday", + "Day Off", + "Sick Day", + + }; + return patternNames[this.scheduleNumber - 1]; + } + + public RealmList<BasalRate> getSchedule() { + return schedule; + } + + public void setSchedule(RealmList<BasalRate> schedule) { + this.schedule = schedule; + } + + public boolean isUploaded() { + return uploaded; + } + + public void setUploaded(boolean uploaded) { + this.uploaded = uploaded; + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpInfo.java b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpInfo.java index 3b25a51612206e6ffc265a1b6e1e18190b056d71..456c6f139830618d580de258f78ac517d3b9258e 100644 --- a/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpInfo.java +++ b/app/src/main/java/info/nightscout/android/model/medtronicNg/PumpInfo.java @@ -1,5 +1,7 @@ package info.nightscout.android.model.medtronicNg; +import android.util.Log; + import io.realm.RealmList; import io.realm.RealmObject; import io.realm.annotations.PrimaryKey; @@ -12,14 +14,16 @@ public class PumpInfo extends RealmObject { private long pumpMac; private String deviceName; private byte lastRadioChannel; + private long lastQueryTS = 0; private RealmList<ContourNextLinkInfo> associatedCnls; - private RealmList<PumpStatusEvent> pumpHistory; + private RealmList<PumpStatusEvent> pumpHistory = new RealmList<>(); + private RealmList<BasalSchedule> basalSchedules; public long getPumpMac() { return pumpMac; } - public void setPumpMac(long pumpMac) { + private void setPumpMac(long pumpMac) { this.pumpMac = pumpMac; } @@ -58,4 +62,38 @@ public class PumpInfo extends RealmObject { public long getPumpSerial() { return pumpMac & 0xffffff; } + + public long getLastQueryTS() { + return lastQueryTS; + } + + public void updateLastQueryTS() { + lastQueryTS = System.currentTimeMillis(); + } + + public RealmList<BasalSchedule> getBasalSchedules() { + return basalSchedules; + } + + public void setBasalSchedules(RealmList<BasalSchedule> basalSchedules) { + this.basalSchedules = basalSchedules; + } + + public boolean checkBasalRatesMatch(PumpStatusEvent pumpRecord) { + byte activeBasal = pumpRecord.getActiveBasalPattern(); + + BasalSchedule schedule = basalSchedules + .where() + .equalTo("scheduleNumber", activeBasal) + .findFirst(); + + if(schedule == null) { + Log.d("Schedule Check", "Didn't find a matching schedule for " + activeBasal); + return false; + } else { + Log.d("Schedule Check", "Found a schedule for " + activeBasal + " with name " + schedule.getName()); + return true; + } + } + } 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 ab95d50f6d346d506d3a6f03a992e53cfaa00804..87adfa0667c404458262d91ce4f503e29dc9e0a1 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 @@ -9,7 +9,7 @@ import io.realm.annotations.Index; /** * Created by lgoedhart on 4/06/2016. */ -public class PumpStatusEvent extends RealmObject { +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 @@ -81,8 +81,15 @@ public class PumpStatusEvent extends RealmObject { return CGM_TREND.valueOf(cgmTrend); } + public String getCgmTrendString() { + return cgmTrend; + } + public void setCgmTrend(CGM_TREND cgmTrend) { - this.cgmTrend = cgmTrend.name(); + if (cgmTrend != null) + this.cgmTrend = cgmTrend.name(); + else + this.cgmTrend = CGM_TREND.NOT_SET.name(); } public void setCgmTrend(String cgmTrend) { @@ -272,6 +279,27 @@ public class PumpStatusEvent extends RealmObject { DOUBLE_DOWN, NOT_COMPUTABLE, RATE_OUT_OF_RANGE, - NOT_SET + NOT_SET; + + public static CGM_TREND fromMessageByte(byte messageByte) { + switch (messageByte) { + case (byte) 0x60: + return PumpStatusEvent.CGM_TREND.FLAT; + case (byte) 0xc0: + return PumpStatusEvent.CGM_TREND.DOUBLE_UP; + case (byte) 0xa0: + return PumpStatusEvent.CGM_TREND.SINGLE_UP; + case (byte) 0x80: + return PumpStatusEvent.CGM_TREND.FOURTY_FIVE_UP; + case (byte) 0x40: + return PumpStatusEvent.CGM_TREND.FOURTY_FIVE_DOWN; + case (byte) 0x20: + return PumpStatusEvent.CGM_TREND.SINGLE_DOWN; + case (byte) 0x00: + return PumpStatusEvent.CGM_TREND.DOUBLE_DOWN; + default: + return PumpStatusEvent.CGM_TREND.NOT_COMPUTABLE; + } + } } } diff --git a/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java b/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java index 7519a5c7f75cc4e177012ca37b2ec08d272d0b8a..cbabb6a4ec7e9a497f487642f0fd8f1f06ccf3da 100644 --- a/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java +++ b/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java @@ -25,11 +25,50 @@ public class SettingsFragment extends PreferenceFragment implements OnSharedPref for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) { initSummary(getPreferenceScreen().getPreference(i)); } + + setMinBatPollIntervall((ListPreference) findPreference("pollInterval"), (ListPreference) findPreference("lowBatPollInterval")); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - updatePrefSummary(findPreference(key)); + Preference pref = findPreference(key); + + + if ("pollInterval".equals(key)) { + setMinBatPollIntervall((ListPreference) pref, (ListPreference) findPreference("lowBatPollInterval")); + } + updatePrefSummary(pref); + } + + // + + /** + * set lowBatPollInterval to normal poll interval at least + * and adapt the selectable values + * + * @param pollIntervalPref + * @param lowBatPollIntervalPref + */ + private void setMinBatPollIntervall(ListPreference pollIntervalPref, ListPreference lowBatPollIntervalPref) { + final String currentValue = lowBatPollIntervalPref.getValue(); + final int pollIntervalPos = (pollIntervalPref.findIndexOfValue(pollIntervalPref.getValue()) >= 0?pollIntervalPref.findIndexOfValue(pollIntervalPref.getValue()):0), + length = pollIntervalPref.getEntries().length; + + CharSequence[] entries = new String[length - pollIntervalPos], + entryValues = new String[length - pollIntervalPos]; + + // generate temp Entries and EntryValues + for(int i = pollIntervalPos; i < length; i++) { + entries[i - pollIntervalPos] = pollIntervalPref.getEntries()[i]; + entryValues[i - pollIntervalPos] = pollIntervalPref.getEntryValues()[i]; + } + lowBatPollIntervalPref.setEntries(entries); + lowBatPollIntervalPref.setEntryValues(entryValues); + + // and set the correct one + if (lowBatPollIntervalPref.findIndexOfValue(currentValue) == -1) { + lowBatPollIntervalPref.setValueIndex(0); + } } @Override 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 new file mode 100644 index 0000000000000000000000000000000000000000..4fe14f2a8542bba2bbd0d346a6faf3663204e003 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java @@ -0,0 +1,181 @@ +package info.nightscout.android.upload.nightscout; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import info.nightscout.android.model.medtronicNg.PumpStatusEvent; +import info.nightscout.android.upload.nightscout.serializer.EntriesSerializer; + +import android.support.annotation.NonNull; + +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.DeviceEndpoints; +import info.nightscout.api.DeviceEndpoints.Iob; +import info.nightscout.api.DeviceEndpoints.Battery; +import info.nightscout.api.DeviceEndpoints.PumpStatus; +import info.nightscout.api.DeviceEndpoints.PumpInfo; +import info.nightscout.api.DeviceEndpoints.DeviceStatus; +import okhttp3.ResponseBody; +import retrofit2.Response; +import retrofit2.Retrofit; + +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()); + + NightScoutUpload() { + + } + + Boolean doRESTUpload(String url, + String secret, + 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; + } + + + private boolean isUploaded(List<PumpStatusEvent> records, + String baseURL, + String secret, + int uploaderBatteryLevel) throws Exception { + + UploadApi uploadApi = new UploadApi(baseURL, formToken(secret)); + + boolean eventsUploaded = uploadEvents(uploadApi.getGlucoseEndpoints(), + uploadApi.getBolusEndpoints(), + records); + + boolean deviceStatusUploaded = uploadDeviceStatus(uploadApi.getDeviceEndpoints(), + uploaderBatteryLevel, records); + + 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<>(); + + 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.getBolusWizardBGL() != 0) { + BolusEntry bolusEntry = new BolusEntry(); + + bolusEntry.setType("mbg"); + bolusEntry.setDate(record.getEventDate().getTime()); + bolusEntry.setDateString(record.getEventDate().toString()); + bolusEntry.setDevice(record.getDeviceName()); + bolusEntry.setMbg(record.getBolusWizardBGL()); + + bolusEntries.add(bolusEntry); + } + + } + + boolean uploaded = true; + if (glucoseEntries.size() > 0) { + Response<ResponseBody> result = glucoseEndpoints.sendEntries(glucoseEntries).execute(); + uploaded = uploaded && result.isSuccessful(); + } + if (bolusEntries.size() > 0) { + Response<ResponseBody> result = bolusEndpoints.sendEntries(bolusEntries).execute(); + uploaded = uploaded && result.isSuccessful(); + } + return uploaded; + } + + private boolean uploadDeviceStatus(DeviceEndpoints deviceEndpoints, + int uploaderBatteryLevel, + List<PumpStatusEvent> records) throws Exception { + + + List<DeviceStatus> deviceEntries = new ArrayList<>(); + for (PumpStatusEvent record : records) { + + Iob iob = new Iob(record.getPumpDate(), record.getActiveInsulin()); + Battery battery = new Battery(record.getBatteryPercentage()); + PumpStatus pumpstatus; + if (record.isBolusing()) { + pumpstatus = new PumpStatus(true, false, ""); + + } else if (record.isSuspended()) { + pumpstatus = new PumpStatus(false, true, ""); + } else { + pumpstatus = new PumpStatus(false, false, "normal"); + } + + PumpInfo pumpInfo = new PumpInfo( + ISO8601_DATE_FORMAT.format(record.getPumpDate()), + new BigDecimal(record.getReservoirAmount()).setScale(3, BigDecimal.ROUND_HALF_UP), + iob, + battery, + pumpstatus + ); + + DeviceStatus deviceStatus = new DeviceStatus( + uploaderBatteryLevel, + record.getDeviceName(), + ISO8601_DATE_FORMAT.format(record.getPumpDate()), + pumpInfo + ); + + deviceEntries.add(deviceStatus); + } + + boolean uploaded = true; + for (DeviceStatus status : deviceEntries) { + Response<ResponseBody> result = deviceEndpoints.sendDeviceStatus(status).execute(); + uploaded = uploaded && result.isSuccessful(); + } + + return uploaded; + } + + @NonNull + private String formToken(String secret) throws NoSuchAlgorithmException, UnsupportedEncodingException { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] bytes = secret.getBytes("UTF-8"); + digest.update(bytes, 0, bytes.length); + bytes = digest.digest(); + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } + +} diff --git a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java index c613dfbb5112dad61676c85cd04ebe520c9b56af..07cba2e8fdbe2f7bc0ae100d436fdb619b78d910 100644 --- a/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightscoutApi.java @@ -2,7 +2,6 @@ package info.nightscout.android.upload.nightscout; import retrofit2.Call; import retrofit2.http.GET; - /** * Created by lgoedhart on 26/06/2016. */ 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 6ac7129878dfdcf45118b3f1f4cf83538766de24..51174e3cfe59108f0b24fc827e0f620f6363e424 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 @@ -10,42 +10,19 @@ import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicResponseHandler; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.math.BigDecimal; -import java.net.URL; -import java.security.MessageDigest; -import java.text.SimpleDateFormat; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import info.nightscout.android.R; import info.nightscout.android.medtronic.MainActivity; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; -import info.nightscout.android.upload.nightscout.serializer.EntriesSerializer; import io.realm.Realm; import io.realm.RealmResults; public class NightscoutUploadIntentService extends IntentService { 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 int SOCKET_TIMEOUT = 60 * 1000; - private static final int CONNECTION_TIMEOUT = 30 * 1000; - Context mContext; + + private Context mContext; private Realm mRealm; + private NightScoutUpload mNightScoutUpload; public NightscoutUploadIntentService() { super(NightscoutUploadIntentService.class.getName()); @@ -64,6 +41,9 @@ public class NightscoutUploadIntentService extends IntentService { Log.i(TAG, "onCreate called"); mContext = this.getBaseContext(); + + mNightScoutUpload = new NightScoutUpload(); + } @Override @@ -85,7 +65,18 @@ public class NightscoutUploadIntentService extends IntentService { if (enableRESTUpload) { long start = System.currentTimeMillis(); Log.i(TAG, String.format("Starting upload of %s record using a REST API", records.size())); - doRESTUpload(prefs, records); + String urlSetting = prefs.getString(mContext.getString(R.string.preference_nightscout_url), ""); + String secretSetting = prefs.getString(mContext.getString(R.string.preference_api_secret), "YOURAPISECRET"); + int uploaderBatteryLevel = MainActivity.batLevel; + Boolean uploadSuccess = mNightScoutUpload.doRESTUpload(urlSetting, + secretSetting, uploaderBatteryLevel, records); + if (uploadSuccess) { + mRealm.beginTransaction(); + for (PumpStatusEvent updateRecord : records) { + updateRecord.setUploaded(true); + } + mRealm.commitTransaction(); + } Log.i(TAG, String.format("Finished upload of %s record using a REST API in %s ms", records.size(), System.currentTimeMillis() - start)); } } catch (Exception e) { @@ -94,221 +85,11 @@ public class NightscoutUploadIntentService extends IntentService { } else { Log.i(TAG, "No records has to be uploaded"); } + mRealm.close(); NightscoutUploadReceiver.completeWakefulIntent(intent); } - private void doRESTUpload(SharedPreferences prefs, RealmResults<PumpStatusEvent> records) { - String apiScheme = "https://"; - String apiUrl = ""; - String apiSecret = prefs.getString(mContext.getString(R.string.preference_api_secret), "YOURAPISECRET"); - - // TODO - this code needs to go to the Settings Activity. - // Add the extra match for "KEY@" to support the previous single field - Pattern p = Pattern.compile("(.*\\/\\/)?(.*@)?([^\\/]*)(.*)"); - Matcher m = p.matcher(prefs.getString(mContext.getString(R.string.preference_nightscout_url), "")); - - if (m.find()) { - apiUrl = m.group(3); - - // Only override apiSecret from URL (the "old" way), if the API secret preference is empty - if (apiSecret.equals("YOURAPISECRET") || apiSecret.equals("")) { - apiSecret = (m.group(2) == null) ? "" : m.group(2).replace("@", ""); - } - - // Override the URI scheme if it's been provided in the preference) - if (m.group(1) != null && !m.group(1).equals("")) { - apiScheme = m.group(1); - } - } - - // Update the preferences to match what we expect. Only really used from converting from the - // old format to the new format. Aren't we nice for managing backward compatibility? - prefs.edit().putString(mContext.getString(R.string.preference_api_secret), apiSecret).apply(); - prefs.edit().putString(mContext.getString(R.string.preference_nightscout_url), String.format("%s%s", apiScheme, apiUrl)).apply(); - - String uploadUrl = String.format("%s%s@%s/api/v1/", apiScheme, apiSecret, apiUrl); - - try { - doRESTUploadTo(uploadUrl, records); - } catch (Exception e) { - Log.e(TAG, "Unable to do REST API Upload to: " + uploadUrl, e); - } - } - - private void doRESTUploadTo(String baseURI, RealmResults<PumpStatusEvent> records) { - try { - String baseURL; - String secret = null; - String[] uriParts = baseURI.split("@"); - - if (uriParts.length == 1) { - throw new Exception("Starting with API v1, a pass phase is required"); - } else if (uriParts.length == 2) { - secret = uriParts[0]; - baseURL = uriParts[1]; - - // new format URL! - if (secret.contains("http")) { - if (secret.contains("https")) { - baseURL = "https://" + baseURL; - } else { - baseURL = "http://" + baseURL; - } - String[] uriParts2 = secret.split("//"); - secret = uriParts2[1]; - } - } else { - throw new Exception(String.format("Unexpected baseURI: %s, uriParts.length: %s", baseURI, uriParts.length)); - } - - JSONArray devicestatusBody = new JSONArray(); - JSONArray entriesBody = new JSONArray(); - - for (PumpStatusEvent record : records) { - addDeviceStatus(devicestatusBody, record); - addSgvEntry(entriesBody, record); - addMbgEntry(entriesBody, record); - } - - boolean isUploaded = uploadToNightscout(new URL(baseURL + "/entries"), secret, entriesBody); - - for(int i = 0; isUploaded && i < devicestatusBody.length(); i++) { - isUploaded &= uploadToNightscout(new URL(baseURL + "/devicestatus"), secret, devicestatusBody.getJSONObject(i)); - } - - if (isUploaded) { - // Yay! We uploaded. Tell Realm - // FIXME - check the upload succeeded! - mRealm.beginTransaction(); - for (PumpStatusEvent updateRecord : records) { - updateRecord.setUploaded(true); - } - mRealm.commitTransaction(); - } - - } catch (Exception e) { - Log.e(TAG, "Unable to post data", e); - } - } - - private boolean uploadToNightscout(URL endpoint, String secret, JSONObject httpBody) throws Exception { - return uploadToNightscout(endpoint, secret, httpBody.toString()); - } - - private boolean uploadToNightscout(URL endpoint, String secret, JSONArray httpBody) throws Exception { - return uploadToNightscout(endpoint, secret, httpBody.toString()); - } - - private boolean uploadToNightscout(URL endpoint, String secret, String httpBody) throws Exception { - Log.i(TAG, "postURL: " + endpoint.toString()); - - HttpPost post = new HttpPost(endpoint.toString()); - - if (secret == null || secret.isEmpty()) { - throw new Exception("Starting with API v1, a pass phase is required"); - } else { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] bytes = secret.getBytes("UTF-8"); - digest.update(bytes, 0, bytes.length); - bytes = digest.digest(); - StringBuilder sb = new StringBuilder(bytes.length * 2); - for (byte b : bytes) { - sb.append(String.format("%02x", b & 0xff)); - } - String token = sb.toString(); - post.setHeader("api-secret", token); - } - - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); - HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); - - DefaultHttpClient httpclient = new DefaultHttpClient(params); - - Log.i(TAG, "Upload JSON: " + httpBody); - - try { - StringEntity se = new StringEntity(httpBody); - post.setEntity(se); - post.setHeader("Accept", "application/json"); - post.setHeader("Content-type", "application/json"); - - ResponseHandler responseHandler = new BasicResponseHandler(); - httpclient.execute(post, responseHandler); - } catch (Exception e) { - Log.w(TAG, "Unable to post data to: '" + post.getURI().toString() + "'", e); - return false; - } - - return true; - } - - private void addDeviceStatus(JSONArray devicestatusArray, PumpStatusEvent record) throws Exception { - JSONObject json = new JSONObject(); - json.put("uploaderBattery", MainActivity.batLevel); - json.put("device", record.getDeviceName()); - json.put("created_at", ISO8601_DATE_FORMAT.format(record.getPumpDate())); - - JSONObject pumpInfo = new JSONObject(); - pumpInfo.put("clock", ISO8601_DATE_FORMAT.format(record.getPumpDate())); - pumpInfo.put("reservoir", new BigDecimal(record.getReservoirAmount()).setScale(3, BigDecimal.ROUND_HALF_UP)); - - JSONObject iob = new JSONObject(); - iob.put("timestamp", record.getPumpDate()); - iob.put("bolusiob", record.getActiveInsulin()); - - JSONObject status = new JSONObject(); - if (record.isBolusing()) { - status.put("bolusing", true); - } else if (record.isSuspended()) { - status.put("suspended", true); - } else { - status.put("status", "normal"); - } - - JSONObject battery = new JSONObject(); - battery.put("percent", record.getBatteryPercentage()); - - pumpInfo.put("iob", iob); - pumpInfo.put("battery", battery); - pumpInfo.put("status", status); - - json.put("pump", pumpInfo); - String jsonString = json.toString(); - Log.i(TAG, "Device Status JSON: " + jsonString); - - devicestatusArray.put(json); - } - - 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.getEventDate().getTime()); - json.put("dateString", pumpRecord.getEventDate()); - - entriesArray.put(json); - } - - private void addMbgEntry(JSONArray entriesArray, PumpStatusEvent pumpRecord) throws Exception { - if (pumpRecord.hasRecentBolusWizard()) { - JSONObject json = new JSONObject(); - - // TODO replace with Retrofit/EntriesSerializer - json.put("type", "mbg"); - json.put("mbg", pumpRecord.getBolusWizardBGL()); - json.put("device", pumpRecord.getDeviceName()); - json.put("date", pumpRecord.getEventDate().getTime()); - json.put("dateString", pumpRecord.getEventDate()); - - entriesArray.put(json); - } - } - private boolean isOnline() { ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo netInfo = cm.getActiveNetworkInfo(); @@ -320,4 +101,6 @@ 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 260b8c3b2acab0a24e81dd6c35dee9f9872f4e21..ce79ab7900ef741ecb3c8f35550eebd019f34d69 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 @@ -42,6 +42,35 @@ public class EntriesSerializer implements JsonSerializer<PumpStatusEvent> { } } + public static String getDirectionStringStatus(PumpStatusEvent.CGM_TREND trend) { + switch( trend ) { + case NONE: + return "NONE"; + case DOUBLE_UP: + return "DoubleUp"; + case SINGLE_UP: + return "SingleUp"; + case FOURTY_FIVE_UP: + return "FortyFiveUp"; + case FLAT: + return "Flat"; + case FOURTY_FIVE_DOWN: + return "FortyFiveDown"; + case SINGLE_DOWN: + return "SingleDown"; + case DOUBLE_DOWN: + return "DoubleDown"; + case NOT_COMPUTABLE: + return "NOT COMPUTABLE"; + case RATE_OUT_OF_RANGE: + return "RATE OUT OF RANGE"; + case NOT_SET: + return "NONE"; + default: + return "NOT COMPUTABLE"; // TODO - should this be something else? + } + } + @Override public JsonElement serialize(PumpStatusEvent src, Type typeOfSrc, JsonSerializationContext context) { final JsonObject jsonObject = new JsonObject(); diff --git a/app/src/main/java/info/nightscout/android/utils/HexDump.java b/app/src/main/java/info/nightscout/android/utils/HexDump.java index 1e01ce78ed71eaf094579ec3f171fdcab1779ef8..ff441ced24ba8bc11f04cc1a57f7ec99a032302a 100644 --- a/app/src/main/java/info/nightscout/android/utils/HexDump.java +++ b/app/src/main/java/info/nightscout/android/utils/HexDump.java @@ -35,7 +35,12 @@ public class HexDump { byte[] line = new byte[16]; int lineIndex = 0; + result.append("\n "); + for (int i = 0; i < Math.min(16, array.length); i++) { + result.append(" ?" + HEX_DIGITS[i]); + } result.append("\n0x"); + result.append(toHexString(offset)); for (int i = offset; i < offset + length; i++) { @@ -63,19 +68,17 @@ public class HexDump { line[lineIndex++] = b; } - if (lineIndex != 16) { - int count = (16 - lineIndex) * 3; - count++; - for (int i = 0; i < count; i++) { - result.append(" "); - } + int count = (16 - lineIndex) * 3; + count++; + for (int i = 0; i < count; i++) { + result.append(" "); + } - for (int i = 0; i < lineIndex; i++) { - if (line[i] > ' ' && line[i] < '~') { - result.append(new String(line, i, 1)); - } else { - result.append("."); - } + for (int i = 0; i < lineIndex; i++) { + if (line[i] > ' ' && line[i] < '~') { + result.append(new String(line, i, 1)); + } else { + result.append("."); } } 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 6cc1a6f91b54362685926b06cb6f9072b78704bd..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); } @@ -85,10 +86,12 @@ public class XDripPlusUploadIntentService extends IntentService { addMbgEntry(entriesBody, record); } - if (entriesBody.length() > 0) sendBundle(mContext, "add", "entries", entriesBody); - if (devicestatusBody.length() > 0) + if (entriesBody.length() > 0) { + sendBundle(mContext, "add", "entries", entriesBody); + } + if (devicestatusBody.length() > 0) { sendBundle(mContext, "add", "devicestatus", devicestatusBody); - + } } catch (Exception e) { Log.e(TAG, "Unable to send bundle: " + e); } @@ -104,8 +107,10 @@ public class XDripPlusUploadIntentService extends IntentService { context.sendBroadcast(intent); List<ResolveInfo> receivers = context.getPackageManager().queryBroadcastReceivers(intent, 0); if (receivers.size() < 1) { - Log.e(TAG, "No receivers"); - } else Log.e(TAG, receivers.size() + " receivers"); + Log.w(TAG, "No xDrip receivers found. "); + } else { + Log.d(TAG, receivers.size() + " xDrip receivers"); + } } diff --git a/app/src/main/java/info/nightscout/api/BolusEndpoints.java b/app/src/main/java/info/nightscout/api/BolusEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..a1c2a56e35eacaa534b5e5ede7de4fca577672c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/api/BolusEndpoints.java @@ -0,0 +1,74 @@ +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 BolusEndpoints { + + class BolusEntry { + String type; + String dateString; + long date; + int mbg; + String device; + + public BolusEntry() { } + + 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 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({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/entries") + Call<ResponseBody> sendEntries(@Body List<BolusEntry> entries); + +} + + + diff --git a/app/src/main/java/info/nightscout/api/DeviceEndpoints.java b/app/src/main/java/info/nightscout/api/DeviceEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..1bf66b24336e790d69c464ed1acb419c029d433d --- /dev/null +++ b/app/src/main/java/info/nightscout/api/DeviceEndpoints.java @@ -0,0 +1,97 @@ +package info.nightscout.api; + +import java.math.BigDecimal; +import java.util.Date; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface DeviceEndpoints { + + class Iob { + final Date timestamp; + final float bolusiob; + public Iob (Date timestamp, + float bolusiob) { + this.timestamp = timestamp; + this.bolusiob = bolusiob; + } + } + + class Battery { + final short percent; + public Battery(short percent) { + this.percent = percent; + } + } + + class PumpStatus { + final Boolean bolusing; + final Boolean suspended; + final String status; + public PumpStatus( + Boolean bolusing, + Boolean suspended, + String status + + ) { + this.bolusing = bolusing; + this.suspended = suspended; + this.status = status; + + } + } + + class PumpInfo { + final String clock; + final BigDecimal reservoir; + final Iob iob; + final Battery battery; + final PumpStatus status; + + public PumpInfo(String clock, + BigDecimal reservoir, + Iob iob, + Battery battery, + PumpStatus status) { + this.clock = clock; + this.reservoir = reservoir; + this.iob = iob; + this.battery = battery; + this.status = status; + + } + } + + class DeviceStatus { + final Integer uploaderBattery; + final String device; + final String created_at; + final PumpInfo pump; + + public DeviceStatus(Integer uploaderBattery, + String device, + String created_at, + PumpInfo pump) { + this.uploaderBattery = uploaderBattery; + this.device = device; + this.created_at = created_at; + this.pump = pump; + } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/devicestatus") + Call<ResponseBody> sendDeviceStatus(@Body DeviceStatus deviceStatus); + +} + + + diff --git a/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java b/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java new file mode 100644 index 0000000000000000000000000000000000000000..d34c5e7abdae5efadc346bc560eb54aee252dacb --- /dev/null +++ b/app/src/main/java/info/nightscout/api/GlucoseEndpoints.java @@ -0,0 +1,83 @@ +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 GlucoseEndpoints { + + class GlucoseEntry { + + String type; + String dateString; + long date; + int sgv; + 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() { } + } + + @Headers({ + "Accept: application/json", + "Content-type: application/json" + }) + @POST("/api/v1/entries") + Call<ResponseBody> sendEntries(@Body List<GlucoseEntry> entries); + +} + + + diff --git a/app/src/main/java/info/nightscout/api/UploadApi.java b/app/src/main/java/info/nightscout/api/UploadApi.java new file mode 100644 index 0000000000000000000000000000000000000000..165203fc7f6f8101b78dc9130f0f5447d5e10f9c --- /dev/null +++ b/app/src/main/java/info/nightscout/api/UploadApi.java @@ -0,0 +1,77 @@ +package info.nightscout.api; + + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class UploadApi { + private Retrofit retrofit; + private GlucoseEndpoints glucoseEndpoints; + private BolusEndpoints bolusEndpoints; + private DeviceEndpoints deviceEndpoints; + + public GlucoseEndpoints getGlucoseEndpoints() { + return glucoseEndpoints; + } + + public BolusEndpoints getBolusEndpoints() { + return bolusEndpoints; + } + + public DeviceEndpoints getDeviceEndpoints() { + return deviceEndpoints; + } + + public UploadApi(String baseURL, String token) { + + class AddAuthHeader implements Interceptor { + + private String token; + + public AddAuthHeader(String token) { + this.token = token; + } + + @Override + public Response intercept(Interceptor.Chain chain) throws IOException { + Request original = chain.request(); + + Request.Builder requestBuilder = original.newBuilder() + .header("api-secret", token) + .method( original.method(), original.body()); + + Request request = requestBuilder.build(); + return chain.proceed(request); + } + }; + + OkHttpClient.Builder okHttpClient = new OkHttpClient().newBuilder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS); + + okHttpClient.addInterceptor(new AddAuthHeader(token)); + + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); + logging.setLevel(HttpLoggingInterceptor.Level.BODY); + okHttpClient.addInterceptor(logging); + + retrofit = new Retrofit.Builder() + .baseUrl(baseURL) + .client(okHttpClient.build()) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + glucoseEndpoints = retrofit.create(GlucoseEndpoints.class); + bolusEndpoints = retrofit.create(BolusEndpoints.class); + deviceEndpoints = retrofit.create(DeviceEndpoints.class); + } +} diff --git a/app/src/main/res/drawable-hdpi/battery_0.png b/app/src/main/res/drawable-hdpi/battery_0.png new file mode 100644 index 0000000000000000000000000000000000000000..c1bc1e661db06dca60a1bfa56459b1341bcc1a7d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/battery_0.png differ diff --git a/app/src/main/res/drawable-hdpi/battery_100.png b/app/src/main/res/drawable-hdpi/battery_100.png new file mode 100644 index 0000000000000000000000000000000000000000..6f76257ee2f3faa17e9133b74644a21e205e7a7b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/battery_100.png differ diff --git a/app/src/main/res/drawable-hdpi/battery_25.png b/app/src/main/res/drawable-hdpi/battery_25.png new file mode 100644 index 0000000000000000000000000000000000000000..ea92908aff0ce281fd9bc844ac1005f389954071 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/battery_25.png differ diff --git a/app/src/main/res/drawable-hdpi/battery_50.png b/app/src/main/res/drawable-hdpi/battery_50.png new file mode 100644 index 0000000000000000000000000000000000000000..691875ac35fe3a599785ad4b138ddae5788d9e79 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/battery_50.png differ diff --git a/app/src/main/res/drawable-hdpi/battery_75.png b/app/src/main/res/drawable-hdpi/battery_75.png new file mode 100644 index 0000000000000000000000000000000000000000..b118fc431c19d8edde4b0a254a218c1718d33847 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/battery_75.png differ diff --git a/app/src/main/res/drawable-hdpi/battery_unknown.png b/app/src/main/res/drawable-hdpi/battery_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..b5eb90f1dc573cd34e7a1cebc036b47c56677d8a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/battery_unknown.png differ diff --git a/app/src/main/res/drawable-mdpi/battery_0.png b/app/src/main/res/drawable-mdpi/battery_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b75d354afde7ab209622e5ec3c864a5973e1add7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/battery_0.png differ diff --git a/app/src/main/res/drawable-mdpi/battery_100.png b/app/src/main/res/drawable-mdpi/battery_100.png new file mode 100644 index 0000000000000000000000000000000000000000..8d54af3c89b52434f2d4e8d216338a56c9a1f9b5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/battery_100.png differ diff --git a/app/src/main/res/drawable-mdpi/battery_25.png b/app/src/main/res/drawable-mdpi/battery_25.png new file mode 100644 index 0000000000000000000000000000000000000000..86347a621bb7cee942aab6bed8854f4ff0e397fd Binary files /dev/null and b/app/src/main/res/drawable-mdpi/battery_25.png differ diff --git a/app/src/main/res/drawable-mdpi/battery_50.png b/app/src/main/res/drawable-mdpi/battery_50.png new file mode 100644 index 0000000000000000000000000000000000000000..7668c659db0ace4ec891e5badf9a00d3ac61fb72 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/battery_50.png differ diff --git a/app/src/main/res/drawable-mdpi/battery_75.png b/app/src/main/res/drawable-mdpi/battery_75.png new file mode 100644 index 0000000000000000000000000000000000000000..d928b5c1c26fbd620e8fd10090a65b82d4778e06 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/battery_75.png differ diff --git a/app/src/main/res/drawable-mdpi/battery_unknown.png b/app/src/main/res/drawable-mdpi/battery_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..8bc91464617a716f8fa5f9aa19a79c214644b324 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/battery_unknown.png differ diff --git a/app/src/main/res/drawable-xhdpi/battery_0.png b/app/src/main/res/drawable-xhdpi/battery_0.png new file mode 100644 index 0000000000000000000000000000000000000000..2c552d7aaf30fd5235aab8551e1e831b1caa4f98 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/battery_0.png differ diff --git a/app/src/main/res/drawable-xhdpi/battery_100.png b/app/src/main/res/drawable-xhdpi/battery_100.png new file mode 100644 index 0000000000000000000000000000000000000000..ca9172be7ace10190853e0d6b460e3bf034c38a5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/battery_100.png differ diff --git a/app/src/main/res/drawable-xhdpi/battery_25.png b/app/src/main/res/drawable-xhdpi/battery_25.png new file mode 100644 index 0000000000000000000000000000000000000000..2bfce679ddb28ee2da3bf13763ca82965fec2bb5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/battery_25.png differ diff --git a/app/src/main/res/drawable-xhdpi/battery_50.png b/app/src/main/res/drawable-xhdpi/battery_50.png new file mode 100644 index 0000000000000000000000000000000000000000..6e0d3b3186fdfad8e1aaadf1bb95e0d2c523bd95 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/battery_50.png differ diff --git a/app/src/main/res/drawable-xhdpi/battery_75.png b/app/src/main/res/drawable-xhdpi/battery_75.png new file mode 100644 index 0000000000000000000000000000000000000000..3cf8966e9f7d08719dee7a8c6febd2ce3557802d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/battery_75.png differ diff --git a/app/src/main/res/drawable-xhdpi/battery_unknown.png b/app/src/main/res/drawable-xhdpi/battery_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..368a8527f8736ae47b26f61fb7bb0d27dde17c39 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/battery_unknown.png differ diff --git a/app/src/main/res/drawable/battery_0.png b/app/src/main/res/drawable/battery_0.png new file mode 100644 index 0000000000000000000000000000000000000000..03c55826d4bab007dafd9ff69e4a30738e78f9ab Binary files /dev/null and b/app/src/main/res/drawable/battery_0.png differ diff --git a/app/src/main/res/drawable/battery_100.png b/app/src/main/res/drawable/battery_100.png new file mode 100644 index 0000000000000000000000000000000000000000..3447a8f32435312acd6c002214716e14c41a62a3 Binary files /dev/null and b/app/src/main/res/drawable/battery_100.png differ diff --git a/app/src/main/res/drawable/battery_25.png b/app/src/main/res/drawable/battery_25.png new file mode 100644 index 0000000000000000000000000000000000000000..1b8e10542a6b0c132f7ff4eb918bc2cef68043e1 Binary files /dev/null and b/app/src/main/res/drawable/battery_25.png differ diff --git a/app/src/main/res/drawable/battery_50.png b/app/src/main/res/drawable/battery_50.png new file mode 100644 index 0000000000000000000000000000000000000000..d516ea0cfe6949fa3b0d518a4a65d846f7337294 Binary files /dev/null and b/app/src/main/res/drawable/battery_50.png differ diff --git a/app/src/main/res/drawable/battery_75.png b/app/src/main/res/drawable/battery_75.png new file mode 100644 index 0000000000000000000000000000000000000000..d2e1ac09daee07c0b0493fcb7ab765798ebba524 Binary files /dev/null and b/app/src/main/res/drawable/battery_75.png differ diff --git a/app/src/main/res/drawable/battery_unknown.png b/app/src/main/res/drawable/battery_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..4c8036c84d3e64b4c6ce5e75c4fb18a00367489c Binary files /dev/null and b/app/src/main/res/drawable/battery_unknown.png differ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml deleted file mode 100644 index 84cc07b2b8603da6a558869fe53fa029ae140bcd..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/activity_login.xml +++ /dev/null @@ -1,32 +0,0 @@ -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center_horizontal" - android:orientation="vertical" - android:paddingBottom="@dimen/activity_vertical_margin" - android:paddingLeft="@dimen/activity_horizontal_margin" - android:paddingRight="@dimen/activity_horizontal_margin" - android:paddingTop="@dimen/activity_vertical_margin" - tools:context=".medtronic.GetHmacAndKeyActivity"> - - <!-- Login progress --> - - <ScrollView - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <LinearLayout - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/registered_usb_devices" /> - - </LinearLayout> - </ScrollView> - -</LinearLayout> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index af14d6804fd460697687fa207ac50f610a4bf518..3dfa26586c1b258eca12ab5d0ba8e3f142d02d82 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -119,28 +119,30 @@ </LinearLayout> - <com.github.mikephil.charting.charts.LineChart - android:id="@+id/chart" + <com.jjoe64.graphview.GraphView android:layout_width="match_parent" - android:layout_height="wrap_content" - android:visibility="invisible" /> + android:layout_height="100dip" + android:id="@+id/chart" /> <ScrollView 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> diff --git a/app/src/main/res/layout/activity_manage_cnl.xml b/app/src/main/res/layout/activity_manage_cnl.xml new file mode 100644 index 0000000000000000000000000000000000000000..41cbee62fa6656797f97d05135d44ba4d57341ab --- /dev/null +++ b/app/src/main/res/layout/activity_manage_cnl.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + + tools:context=".medtronic.ManageCNLActivity"> + + <android.support.design.widget.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> + + <android.support.v7.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" + app:layout_scrollFlags="scroll|enterAlways" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light"> + + </android.support.v7.widget.Toolbar> + + </android.support.design.widget.AppBarLayout> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10sp" + android:layout_marginTop="10sp" + android:baselineAligned="true" + android:gravity="bottom" + android:orientation="vertical"> + + <ListView + android:id="@+id/cnl_list" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + </LinearLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/manage_cnl_listview_empty" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" + android:layout_marginLeft="10sp" + android:layout_marginRight="10sp" + android:visibility="gone"> + + <TextView + android:id="@+id/manage_cnl_listview_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/no_registered_contour_next_link_devices" + android:layout_marginBottom="10sp" + android:textSize="@dimen/materialize_typography_headline" + android:textStyle="bold" /> + + <TextView + android:id="@+id/manage_cnl_listview_text2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/to_register_a_contour_next_link_you_must_first_plug_it_in_and_get_a_reading_from_the_pump" /> + </LinearLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/activity_status.xml b/app/src/main/res/layout/activity_status.xml index eff1081d8b651ab4bebcaae0eab44e4fb08dea67..5a7529d14946eef094b6b350374771d6c953c670 100644 --- a/app/src/main/res/layout/activity_status.xml +++ b/app/src/main/res/layout/activity_status.xml @@ -1,9 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - xmlns:app="http://schemas.android.com/apk/res-auto" + tools:context="info.nightscout.android.medtronic.StatusActivity" android:orientation="vertical"> @@ -28,7 +29,7 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/status_scroll_view" - android:fillViewport="true" > + android:fillViewport="true"> <LinearLayout android:orientation="vertical" diff --git a/app/src/main/res/layout/cnl_item.xml b/app/src/main/res/layout/cnl_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..5ce73b2c268c0bfb9e233f7504435b64cc1c06d1 --- /dev/null +++ b/app/src/main/res/layout/cnl_item.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <TextView + android:id="@+id/cnl_mac" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentLeft="true" + android:paddingLeft="8dp" + android:textSize="18sp" + android:textStyle="bold" /> + + <Button + android:id="@+id/delete_btn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:layout_marginRight="5dp" + android:text="Delete" /> +</RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/manage_cnl_listview_header.xml b/app/src/main/res/layout/manage_cnl_listview_header.xml new file mode 100644 index 0000000000000000000000000000000000000000..163219fc423a3780d9375dfe9ad022b088b5600e --- /dev/null +++ b/app/src/main/res/layout/manage_cnl_listview_header.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/manage_cnl_listview_header" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="10sp" + android:text="@string/serial_number" + android:layout_marginLeft="10sp" /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index 3a4948d2788551963d7d0670e34f0e6c67aa5944..a1da6693ab352108fe3a1f7a42105a3d839f00dd 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -1,6 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + <!-- battery icons by Oxygen Team http://www.oxygen-icons.org --> + <item + android:id="@+id/status_battery" + android:icon="@drawable/battery_unknown" + android:orderInCategory="99" + app:showAsAction="always" + android:title="@string/menu_name_battery_status"/> <item android:id="@+id/action_menu_status" android:icon="@drawable/ic_launcher" diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000000000000000000000000000000000000..cfb3458e897140a9308cac32f15041128630caad --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string-array name="poll_interval"> + <item>5 min</item> + <item>10 min</item> + <item>12 min</item> + <item>15 min</item> + <item>20 min</item> + <item>30 min</item> + <item>60 min</item> + <!--item>off</item--> + </string-array> + + <string-array name="poll_interval_millis"> + <item>300000</item> + <item>600000</item> + <item>720000</item> + <item>900000</item> + <item>1200000</item> + <item>1800000</item> + <item>3600000</item> + <!--item>0</item--> + </string-array> + + <string-array name="chart_zoom"> + <item>1 hour</item> + <item>3 hours</item> + <item>6 hours</item> + <item>12 hours</item> + <item>24 hours</item> + </string-array> + + <string-array name="chart_zoom_hours"> + <item>1</item> + <item>3</item> + <item>6</item> + <item>12</item> + <item>24</item> + </string-array> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 443d000163d74d1afe620bd3cb389b7d005b04b1..b403419f414cf3f94125d7f47ae87e0e7f14e5b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ <item>Info</item> <item>Debug</item> </string-array> - <string name="title_activity_login">CareLink login</string> + <string name="title_activity_manage_cnl">Manage your Contour Next Link devices</string> <!-- Strings related to login --> <string name="prompt_username">CareLink Username</string> @@ -25,9 +25,11 @@ <string name="action_sign_in_short">Retrieve keys</string> <string name="error_invalid_password">Password is required</string> <string name="error_incorrect_password">The Username or password is incorrect</string> + <string name="error_client_protocol_exception">Could not communicate with server.</string> + <string name="error_io_exception">Could not connect to server.</string> + <string name="error_class_not_found_exception">Application code error.</string> + <string name="error_http_response">Server responded with error. Could be username or password problem.</string> <string name="error_field_required">This field is required</string> - <string name="preference_nightscout_url">Nightscout URL</string> - <string name="preference_api_secret">API SECRET</string> <string name="prompt_carelink_username_password">Please enter your CareLink details.\nThey will not be stored.</string> <string name="close">Close</string> <string name="register_contour_next_link">Registered Devices</string> @@ -50,4 +52,15 @@ <string name="dummy_button">Dummy Button</string> <string name="dummy_content">DUMMY\nCONTENT</string> <string name="menu_name_status">Status</string> + <string name="menu_name_battery_status">unknown</string> + <string name="preference_api_secret">YOUR.API.SECRET</string> + <string name="preference_nightscout_url">YOUR.NIGHTSCOUT.URL</string> + + <string name="preferences_poll_interval">Poll interval</string> + <string name="preferences_low_battery_poll_interval">Poll interval on low pump battery</string> + <string name="no_registered_contour_next_link_devices">No registered Contour Next Link devices</string> + <string name="to_register_a_contour_next_link_you_must_first_plug_it_in_and_get_a_reading_from_the_pump">To register a Contour Next Link you must first plug it in, and get a reading from the pump.</string> + <string name="serial_number">Serial number</string> + <string name="preferences_chart_interval">Chart Zoom</string> + </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9e6793c85ec60101ef37b053d3af5d9caefd74b1..de93838a97ceafac7a7701ecfbb6ee914ed9c5c3 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -18,6 +18,37 @@ android:switchTextOff="1" 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"/> + <info.nightscout.android.utils.CustomSwitchPreference + android:disableDependentsState="false" + android:key="doublePollOnPumpAway" + android:summaryOff="Normal polling if pump is away" + android:summaryOn="Double polling if pump is away" + android:switchTextOff="off" + android:switchTextOn="on" + android:title="Polling interval if pump is away"/> + <ListPreference + android:key="lowBatPollInterval" + android:defaultValue="900000" + android:title="@string/preferences_low_battery_poll_interval" + android:summary="%s" + android:entries="@array/poll_interval" + android:entryValues="@array/poll_interval_millis"/> + </PreferenceCategory> + <PreferenceCategory android:title="Display"> + <ListPreference + android:key="chartZoom" + android:defaultValue="3" + android:title="@string/preferences_chart_interval" + android:summary="%s" + android:entries="@array/chart_zoom" + android:entryValues="@array/chart_zoom_hours"/> </PreferenceCategory> <PreferenceCategory android:title="Sharing"> <CheckBoxPreference diff --git a/build.gradle b/build.gradle index 9da42b0b994170c75de5ee547bfabc0007cf8dd7..fa343792dd739dcc26b1521ca4931cdda922476f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { classpath 'com.android.tools.build:gradle:2.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files - classpath "io.realm:realm-gradle-plugin:1.0.0" + classpath 'io.realm:realm-gradle-plugin:2.2.2' } }