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 deleted file mode 100644 index dd11c422309587fbfb2c40dcfbc1c5c64bb0d573..0000000000000000000000000000000000000000 --- a/640gAndroidUploader.iml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module external.linked.project.id="640gAndroidUploader" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> - <component name="FacetManager"> - <facet type="java-gradle" name="Java-Gradle"> - <configuration> - <option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" /> - <option name="BUILDABLE" value="false" /> - </configuration> - </facet> - </component> - <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true"> - <exclude-output /> - <content url="file://$MODULE_DIR$"> - <excludeFolder url="file://$MODULE_DIR$/.gradle" /> - </content> - <orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" /> - <orderEntry type="sourceFolder" forTests="false" /> - </component> -</module> \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2346e1e45da68fc2d530770a87cd25ad3eea5b02..b5c94546d6e2e01b5d16df7d4f2c716deb630dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,6 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - Initial public beta release -[Unreleased]: https://github.com/pazaan/640gAndroidUploader/compare/v0.2.0...HEAD -[v0.2.0]: https://github.com/pazaan/640gAndroidUploader/compare/v0.1.1...v0.2.0 -[v0.1.1]: https://github.com/pazaan/640gAndroidUploader/compare/v0.1.0...v0.1.1 +[Unreleased]: https://github.com/pazaan/600SeriesAndroidUploader/compare/v0.2.0...HEAD +[v0.2.0]: https://github.com/pazaan/600SeriesAndroidUploader/compare/v0.1.1...v0.2.0 +[v0.1.1]: https://github.com/pazaan/600SeriesAndroidUploader/compare/v0.1.0...v0.1.1 diff --git a/app/640gUploader.iml b/app/640gUploader.iml deleted file mode 100644 index 45575b43f07bf114a0d826ad3cb7d363481e01df..0000000000000000000000000000000000000000 --- a/app/640gUploader.iml +++ /dev/null @@ -1,120 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module external.linked.project.id=":640gUploader" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="Medtronic640gUploader" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> - <component name="FacetManager"> - <facet type="android-gradle" name="Android-Gradle"> - <configuration> - <option name="GRADLE_PROJECT_PATH" value=":640gUploader" /> - </configuration> - </facet> - <facet type="android" name="Android"> - <configuration> - <option name="SELECTED_BUILD_VARIANT" value="debug" /> - <option name="SELECTED_TEST_ARTIFACT" value="_android_test_" /> - <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> - <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" /> - <afterSyncTasks> - <task>generateDebugSources</task> - </afterSyncTasks> - <option name="ALLOW_USER_CONFIGURATION" value="false" /> - <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> - <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" /> - <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" /> - <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" /> - </configuration> - </facet> - </component> - <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false"> - <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" /> - <output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/debug" /> - <exclude-output /> - <content url="file://$MODULE_DIR$"> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/jni" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/jni" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" /> - <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/builds" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3/jars" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3/jars" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-classes" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-runtime-classes" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-safeguard" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-verifier" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-support" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/reload-dex" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/restart-dex" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" /> - <excludeFolder url="file://$MODULE_DIR$/build/outputs" /> - <excludeFolder url="file://$MODULE_DIR$/build/tmp" /> - </content> - <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" exported="" name="appcompat-v7-21.0.3" level="project" /> - <orderEntry type="library" exported="" name="mongo-java-driver-3.0.2" level="project" /> - <orderEntry type="library" exported="" name="commons-lang3-3.4" level="project" /> - <orderEntry type="library" exported="" name="support-v4-21.0.3" level="project" /> - <orderEntry type="library" exported="" name="support-annotations-21.0.3" level="project" /> - <orderEntry type="library" exported="" name="slf4j-api-1.7.2" level="project" /> - <orderEntry type="library" exported="" name="logback-android-1.1.1-3" level="project" /> - </component> -</module> \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 321863d615cb36775b630d58e4edd1721a5f7a06..b4d22ec6eb4fc031d5e71d600bcd294eb7fa58ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,17 +2,18 @@ import org.ajoberstar.grgit.Grgit buildscript { repositories { + jcenter() maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath 'io.fabric.tools:gradle:1.21.6' - classpath 'io.realm:realm-gradle-plugin:1.1.1' + classpath 'io.fabric.tools:gradle:1.22.2' + classpath 'io.realm:realm-gradle-plugin:3.4.0' classpath 'org.ajoberstar:grgit:1.5.0' } } plugins { - id 'net.researchgate.release' version '2.3.4' + id 'net.researchgate.release' version '2.6.0' } apply plugin: 'com.android.application' @@ -28,7 +29,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,26 +38,27 @@ 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() { +static def getBugfenderApiKey() { Properties properties = new Properties() properties.load(new FileInputStream("app/bugfender.properties")) return "\"" + properties.getProperty("apiKey", "") + "\"" } android { - compileSdkVersion 23 - buildToolsVersion "23.0.3" - // FIXME - replace with URLConnection. This is used in GetHmacAndKeyActivity. + compileSdkVersion 25 + buildToolsVersion '25.0.2' + // FIXME - replace with URLConnection. This is used in ManageCNLActivity. useLibrary 'org.apache.http.legacy' applicationVariants.all { variant -> @@ -66,8 +68,8 @@ android { defaultConfig { applicationId "info.nightscout.android" minSdkVersion 14 - targetSdkVersion 23 - versionName project.properties['version'] + "/" + gitCommitId() + targetSdkVersion 25 + versionName project.properties['version'] + "/" + gitCommitId() // + " (" + gitBranch()+")" versionCode gitVersion() buildConfigField "String", "BUGFENDER_API_KEY", getBugfenderApiKey() } @@ -86,52 +88,56 @@ android { } } -task signRelease << { - def command = [ - 'jarsigner', - '-verbose', - '-sigalg', - 'SHA1withRSA', - '-digestalg', - 'SHA1', - '-keystore', - '/Users/lennart/keystores/nightscout_android.jks', - 'app/build/outputs/apk/app-release-unsigned.apk', - 'nightscoutandroidkey' - ] - - def proc = new ProcessBuilder(command) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .redirectInput(ProcessBuilder.Redirect.INHERIT) - .redirectError(ProcessBuilder.Redirect.INHERIT) - .start() - - proc.waitFor() - - if (0 != proc.exitValue()) { - throw new RuntimeException("Could not sign APK.") +task signRelease { + doLast { + def command = [ + 'jarsigner', + '-verbose', + '-sigalg', + 'SHA1withRSA', + '-digestalg', + 'SHA1', + '-keystore', + '/Users/lennart/keystores/nightscout_android.jks', + 'app/build/outputs/apk/app-release-unsigned.apk', + 'nightscoutandroidkey' + ] + + def proc = new ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectInput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + + proc.waitFor() + + if (0 != proc.exitValue()) { + throw new RuntimeException("Could not sign APK.") + } } } -task zipalignRelease << { - def command = [ - '/Users/lennart/Library/Android/sdk/build-tools/23.0.3/zipalign', - '-v', - '4', - 'app/build/outputs/apk/app-release-unsigned.apk', - 'app/build/outputs/apk/640g-android-uploader.apk' - ] - - def proc = new ProcessBuilder(command) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .redirectInput(ProcessBuilder.Redirect.INHERIT) - .redirectError(ProcessBuilder.Redirect.INHERIT) - .start() - - proc.waitFor() - - if (0 != proc.exitValue()) { - throw new RuntimeException("Could not align APK.") +task zipalignRelease { + doLast { + def command = [ + '/Users/lennart/Library/Android/sdk/build-tools/25.0.2/zipalign', + '-v', + '4', + 'app/build/outputs/apk/app-release-unsigned.apk', + 'app/build/outputs/apk/600-series-uploader.apk' + ] + + def proc = new ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectInput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + + proc.waitFor() + + if (0 != proc.exitValue()) { + throw new RuntimeException("Could not align APK.") + } } } @@ -144,21 +150,34 @@ release { dependencies { compile files('libs/slf4j-api-1.7.2.jar') - compile('com.crashlytics.sdk.android:crashlytics:2.6.5@aar') { - transitive = true; - } - compile('com.mikepenz:materialdrawer:5.2.9@aar') { - transitive = true - } - compile 'com.android.support:appcompat-v7:23.4.0' + + compile 'com.android.support:support-v13:25.3.1' + compile 'com.android.support:design:25.3.1' + compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:recyclerview-v7:25.3.1' + compile 'com.android.support:cardview-v7:25.3.1' 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.android.support:support-v4:23.4.0' + compile 'com.bugfender.sdk:android:0.7.2' + compile 'com.jjoe64:graphview:4.0.1' 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.crashlytics.sdk.android:crashlytics:2.6.8@aar') { + transitive = true; + } + + // The version of okhttp3 *must* be the same as the version in AppUpdater + compile 'com.squareup.okhttp3:okhttp:3.6.0' + compile 'com.squareup.okhttp3:logging-interceptor:3.6.0' + + compile('com.crashlytics.sdk.android:crashlytics:2.6.7@aar') { + transitive = true; + } + compile('com.mikepenz:materialdrawer:5.2.9@aar') { + transitive = true + } + compile 'com.github.javiersantos:AppUpdater:2.6.1' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7d7185b04799440fc232b3ffd10ef632e76220c..3e5115f5ee899354c561c7788101e4fb9bd054ee 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" /> diff --git a/app/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java b/app/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java new file mode 100644 index 0000000000000000000000000000000000000000..3924ee0bbf610fd9a72a20c66dc31ca6509cb742 --- /dev/null +++ b/app/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java @@ -0,0 +1,506 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.integration.android; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +/** + * <p>A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple + * way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the + * project's source code.</p> + * + * <h2>Initiating a barcode scan</h2> + * + * <p>To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait + * for the result in your app.</p> + * + * <p>It does require that the Barcode Scanner (or work-alike) application is installed. The + * {@link #initiateScan()} method will prompt the user to download the application, if needed.</p> + * + * <p>There are a few steps to using this integration. First, your {@link Activity} must implement + * the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:</p> + * + * <pre>{@code + * public void onActivityResult(int requestCode, int resultCode, Intent intent) { + * IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + * if (scanResult != null) { + * // handle scan result + * } + * // else continue with any other code you need in the method + * ... + * } + * }</pre> + * + * <p>This is where you will handle a scan result.</p> + * + * <p>Second, just call this in response to a user action somewhere to begin the scan process:</p> + * + * <pre>{@code + * IntentIntegrator integrator = new IntentIntegrator(yourActivity); + * integrator.initiateScan(); + * }</pre> + * + * <p>Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the + * user was prompted to download the application. This lets the calling app potentially manage the dialog. + * In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()} + * method.</p> + * + * <p>You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use + * {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and + * yes/no button labels can be changed.</p> + * + * <p>Finally, you can use {@link #addExtra(String, Object)} to add more parameters to the Intent used + * to invoke the scanner. This can be used to set additional options not directly exposed by this + * simplified API.</p> + * + * <p>By default, this will only allow applications that are known to respond to this intent correctly + * do so. The apps that are allowed to response can be set with {@link #setTargetApplications(List)}. + * For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.</p> + * + * <h2>Sharing text via barcode</h2> + * + * <p>To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.</p> + * + * <p>Some code, particularly download integration, was contributed from the Anobiit application.</p> + * + * <h2>Enabling experimental barcode formats</h2> + * + * <p>Some formats are not enabled by default even when scanning with {@link #ALL_CODE_TYPES}, such as + * PDF417. Use {@link #initiateScan(Collection)} with + * a collection containing the names of formats to scan for explicitly, like "PDF_417", to use such + * formats.</p> + * + * @author Sean Owen + * @author Fred Lin + * @author Isaac Potoczny-Jones + * @author Brad Drehmer + * @author gcstang + */ +public class IntentIntegrator { + + public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits + private static final String TAG = IntentIntegrator.class.getSimpleName(); + + public static final String DEFAULT_TITLE = "Install Barcode Scanner?"; + public static final String DEFAULT_MESSAGE = + "This application requires Barcode Scanner. Would you like to install it?"; + public static final String DEFAULT_YES = "Yes"; + public static final String DEFAULT_NO = "No"; + + private static final String BS_PACKAGE = "com.google.zxing.client.android"; + private static final String BSPLUS_PACKAGE = "com.srowen.bs.android"; + + // supported barcode formats + public static final Collection<String> PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14"); + public static final Collection<String> ONE_D_CODE_TYPES = + list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128", + "ITF", "RSS_14", "RSS_EXPANDED"); + public static final Collection<String> QR_CODE_TYPES = Collections.singleton("QR_CODE"); + public static final Collection<String> DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX"); + + public static final Collection<String> ALL_CODE_TYPES = null; + + public static final List<String> TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE); + public static final List<String> TARGET_ALL_KNOWN = list( + BSPLUS_PACKAGE, // Barcode Scanner+ + BSPLUS_PACKAGE + ".simple", // Barcode Scanner+ Simple + BS_PACKAGE // Barcode Scanner + // What else supports this intent? + ); + + private final Activity activity; + private final Fragment fragment; + + private String title; + private String message; + private String buttonYes; + private String buttonNo; + private List<String> targetApplications; + private final Map<String,Object> moreExtras = new HashMap<>(3); + + /** + * @param activity {@link Activity} invoking the integration + */ + public IntentIntegrator(Activity activity) { + this.activity = activity; + this.fragment = null; + initializeConfiguration(); + } + + /** + * @param fragment {@link Fragment} invoking the integration. + * {@link #startActivityForResult(Intent, int)} will be called on the {@link Fragment} instead + * of an {@link Activity} + */ + public IntentIntegrator(Fragment fragment) { + this.activity = fragment.getActivity(); + this.fragment = fragment; + initializeConfiguration(); + } + + private void initializeConfiguration() { + title = DEFAULT_TITLE; + message = DEFAULT_MESSAGE; + buttonYes = DEFAULT_YES; + buttonNo = DEFAULT_NO; + targetApplications = TARGET_ALL_KNOWN; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setTitleByID(int titleID) { + title = activity.getString(titleID); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setMessageByID(int messageID) { + message = activity.getString(messageID); + } + + public String getButtonYes() { + return buttonYes; + } + + public void setButtonYes(String buttonYes) { + this.buttonYes = buttonYes; + } + + public void setButtonYesByID(int buttonYesID) { + buttonYes = activity.getString(buttonYesID); + } + + public String getButtonNo() { + return buttonNo; + } + + public void setButtonNo(String buttonNo) { + this.buttonNo = buttonNo; + } + + public void setButtonNoByID(int buttonNoID) { + buttonNo = activity.getString(buttonNoID); + } + + public Collection<String> getTargetApplications() { + return targetApplications; + } + + public final void setTargetApplications(List<String> targetApplications) { + if (targetApplications.isEmpty()) { + throw new IllegalArgumentException("No target applications"); + } + this.targetApplications = targetApplications; + } + + public void setSingleTargetApplication(String targetApplication) { + this.targetApplications = Collections.singletonList(targetApplication); + } + + public Map<String,?> getMoreExtras() { + return moreExtras; + } + + public final void addExtra(String key, Object value) { + moreExtras.put(key, value); + } + + /** + * Initiates a scan for all known barcode types with the default camera. + * + * @return the {@link AlertDialog} that was shown to the user prompting them to download the app + * if a prompt was needed, or null otherwise. + */ + public final AlertDialog initiateScan() { + return initiateScan(ALL_CODE_TYPES, -1); + } + + /** + * Initiates a scan for all known barcode types with the specified camera. + * + * @param cameraId camera ID of the camera to use. A negative value means "no preference". + * @return the {@link AlertDialog} that was shown to the user prompting them to download the app + * if a prompt was needed, or null otherwise. + */ + public final AlertDialog initiateScan(int cameraId) { + return initiateScan(ALL_CODE_TYPES, cameraId); + } + + /** + * Initiates a scan, using the default camera, only for a certain set of barcode types, given as strings corresponding + * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants + * like {@link #PRODUCT_CODE_TYPES} for example. + * + * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for + * @return the {@link AlertDialog} that was shown to the user prompting them to download the app + * if a prompt was needed, or null otherwise. + */ + public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats) { + return initiateScan(desiredBarcodeFormats, -1); + } + + /** + * Initiates a scan, using the specified camera, only for a certain set of barcode types, given as strings corresponding + * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants + * like {@link #PRODUCT_CODE_TYPES} for example. + * + * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for + * @param cameraId camera ID of the camera to use. A negative value means "no preference". + * @return the {@link AlertDialog} that was shown to the user prompting them to download the app + * if a prompt was needed, or null otherwise + */ + public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats, int cameraId) { + Intent intentScan = new Intent(BS_PACKAGE + ".SCAN"); + intentScan.addCategory(Intent.CATEGORY_DEFAULT); + + // check which types of codes to scan for + if (desiredBarcodeFormats != null) { + // set the desired barcode types + StringBuilder joinedByComma = new StringBuilder(); + for (String format : desiredBarcodeFormats) { + if (joinedByComma.length() > 0) { + joinedByComma.append(','); + } + joinedByComma.append(format); + } + intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString()); + } + + // check requested camera ID + if (cameraId >= 0) { + intentScan.putExtra("SCAN_CAMERA_ID", cameraId); + } + + String targetAppPackage = findTargetAppPackage(intentScan); + if (targetAppPackage == null) { + return showDownloadDialog(); + } + intentScan.setPackage(targetAppPackage); + intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intentScan.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + attachMoreExtras(intentScan); + startActivityForResult(intentScan, REQUEST_CODE); + return null; + } + + /** + * Start an activity. This method is defined to allow different methods of activity starting for + * newer versions of Android and for compatibility library. + * + * @param intent Intent to start. + * @param code Request code for the activity + * @see Activity#startActivityForResult(Intent, int) + * @see Fragment#startActivityForResult(Intent, int) + */ + protected void startActivityForResult(Intent intent, int code) { + if (fragment == null) { + activity.startActivityForResult(intent, code); + } else { + fragment.startActivityForResult(intent, code); + } + } + + private String findTargetAppPackage(Intent intent) { + PackageManager pm = activity.getPackageManager(); + List<ResolveInfo> availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (availableApps != null) { + for (String targetApp : targetApplications) { + if (contains(availableApps, targetApp)) { + return targetApp; + } + } + } + return null; + } + + private static boolean contains(Iterable<ResolveInfo> availableApps, String targetApp) { + for (ResolveInfo availableApp : availableApps) { + String packageName = availableApp.activityInfo.packageName; + if (targetApp.equals(packageName)) { + return true; + } + } + return false; + } + + private AlertDialog showDownloadDialog() { + AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity); + downloadDialog.setTitle(title); + downloadDialog.setMessage(message); + downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + String packageName; + if (targetApplications.contains(BS_PACKAGE)) { + // Prefer to suggest download of BS if it's anywhere in the list + packageName = BS_PACKAGE; + } else { + // Otherwise, first option: + packageName = targetApplications.get(0); + } + Uri uri = Uri.parse("market://details?id=" + packageName); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + try { + if (fragment == null) { + activity.startActivity(intent); + } else { + fragment.startActivity(intent); + } + } catch (ActivityNotFoundException anfe) { + // Hmm, market is not installed + Log.w(TAG, "Google Play is not installed; cannot install " + packageName); + } + } + }); + downloadDialog.setNegativeButton(buttonNo, null); + downloadDialog.setCancelable(true); + return downloadDialog.show(); + } + + + /** + * <p>Call this from your {@link Activity}'s + * {@link Activity#onActivityResult(int, int, Intent)} method.</p> + * + * @param requestCode request code from {@code onActivityResult()} + * @param resultCode result code from {@code onActivityResult()} + * @param intent {@link Intent} from {@code onActivityResult()} + * @return null if the event handled here was not related to this class, or + * else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning, + * the fields will be null. + */ + public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + String contents = intent.getStringExtra("SCAN_RESULT"); + String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT"); + byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES"); + int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE); + Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation; + String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL"); + return new IntentResult(contents, + formatName, + rawBytes, + orientation, + errorCorrectionLevel); + } + return new IntentResult(); + } + return null; + } + + + /** + * Defaults to type "TEXT_TYPE". + * + * @param text the text string to encode as a barcode + * @return the {@link AlertDialog} that was shown to the user prompting them to download the app + * if a prompt was needed, or null otherwise + * @see #shareText(CharSequence, CharSequence) + */ + public final AlertDialog shareText(CharSequence text) { + return shareText(text, "TEXT_TYPE"); + } + + /** + * Shares the given text by encoding it as a barcode, such that another user can + * scan the text off the screen of the device. + * + * @param text the text string to encode as a barcode + * @param type type of data to encode. See {@code com.google.zxing.client.android.Contents.Type} constants. + * @return the {@link AlertDialog} that was shown to the user prompting them to download the app + * if a prompt was needed, or null otherwise + */ + public final AlertDialog shareText(CharSequence text, CharSequence type) { + Intent intent = new Intent(); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setAction(BS_PACKAGE + ".ENCODE"); + intent.putExtra("ENCODE_TYPE", type); + intent.putExtra("ENCODE_DATA", text); + String targetAppPackage = findTargetAppPackage(intent); + if (targetAppPackage == null) { + return showDownloadDialog(); + } + intent.setPackage(targetAppPackage); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + attachMoreExtras(intent); + if (fragment == null) { + activity.startActivity(intent); + } else { + fragment.startActivity(intent); + } + return null; + } + + private static List<String> list(String... values) { + return Collections.unmodifiableList(Arrays.asList(values)); + } + + private void attachMoreExtras(Intent intent) { + for (Map.Entry<String,Object> entry : moreExtras.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + // Kind of hacky + if (value instanceof Integer) { + intent.putExtra(key, (Integer) value); + } else if (value instanceof Long) { + intent.putExtra(key, (Long) value); + } else if (value instanceof Boolean) { + intent.putExtra(key, (Boolean) value); + } else if (value instanceof Double) { + intent.putExtra(key, (Double) value); + } else if (value instanceof Float) { + intent.putExtra(key, (Float) value); + } else if (value instanceof Bundle) { + intent.putExtra(key, (Bundle) value); + } else { + intent.putExtra(key, value.toString()); + } + } + } + +} diff --git a/app/src/main/java/com/google/zxing/integration/android/IntentResult.java b/app/src/main/java/com/google/zxing/integration/android/IntentResult.java new file mode 100644 index 0000000000000000000000000000000000000000..15b2e961cdfbb848218261f985dd1edbe76e8ddb --- /dev/null +++ b/app/src/main/java/com/google/zxing/integration/android/IntentResult.java @@ -0,0 +1,93 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.integration.android; + +/** + * <p>Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.</p> + * + * @author Sean Owen + */ +public final class IntentResult { + + private final String contents; + private final String formatName; + private final byte[] rawBytes; + private final Integer orientation; + private final String errorCorrectionLevel; + + IntentResult() { + this(null, null, null, null, null); + } + + IntentResult(String contents, + String formatName, + byte[] rawBytes, + Integer orientation, + String errorCorrectionLevel) { + this.contents = contents; + this.formatName = formatName; + this.rawBytes = rawBytes; + this.orientation = orientation; + this.errorCorrectionLevel = errorCorrectionLevel; + } + + /** + * @return raw content of barcode + */ + public String getContents() { + return contents; + } + + /** + * @return name of format, like "QR_CODE", "UPC_A". See {@code BarcodeFormat} for more format names. + */ + public String getFormatName() { + return formatName; + } + + /** + * @return raw bytes of the barcode content, if applicable, or null otherwise + */ + public byte[] getRawBytes() { + return rawBytes; + } + + /** + * @return rotation of the image, in degrees, which resulted in a successful scan. May be null. + */ + public Integer getOrientation() { + return orientation; + } + + /** + * @return name of the error correction level used in the barcode, if applicable + */ + public String getErrorCorrectionLevel() { + return errorCorrectionLevel; + } + + @Override + public String toString() { + int rawBytesLength = rawBytes == null ? 0 : rawBytes.length; + return "Format: " + formatName + '\n' + + "Contents: " + contents + '\n' + + "Raw bytes: (" + rawBytesLength + " bytes)\n" + + "Orientation: " + orientation + '\n' + + "EC level: " + errorCorrectionLevel + '\n'; + } + +} 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..373489b351c88f124b26921b5d3b02bba0f35699 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,11 +25,10 @@ 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; import android.util.Log; import android.view.Menu; @@ -36,12 +37,17 @@ 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.github.javiersantos.appupdater.AppUpdater; +import com.github.javiersantos.appupdater.enums.UpdateFrom; +import com.jjoe64.graphview.DefaultLabelFormatter; +import com.jjoe64.graphview.GraphView; +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,37 +68,69 @@ 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.MedtronicCnlAlarmReceiver; +import info.nightscout.android.medtronic.service.MedtronicCnlAlarmManager; 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 info.nightscout.android.utils.ConfigurationStore; +import info.nightscout.android.utils.DataStore; 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 int USB_DISCONNECT_NOFICATION_ID = 1; + public static final float MMOLXLFACTOR = 18.016f; + + private DataStore dataStore = DataStore.getInstance(); + private ConfigurationStore configurationStore = ConfigurationStore.getInstance(); + + private int chartZoom = 3; + private boolean hasZoomedChart = false; - public static int batLevel = 0; - private static long activePumpMac; - boolean mEnableCgmService = true; - SharedPreferences prefs = null; + private boolean mEnableCgmService = true; + private SharedPreferences prefs = null; private PumpInfo mActivePump; private TextView mTextViewLog; // This will eventually move to a status page. - private LineChart mChart; - private Intent mNightscoutUploadService; + private GraphView mChart; private Handler mUiRefreshHandler = new Handler(); private Runnable mUiRefreshRunnable = new RefreshDisplayRunnable(); private Realm mRealm; 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.getSgvDate().getTime() + pumpStatusData.getPumpTimeOffset(), + now = System.currentTimeMillis(), + pollInterval = ConfigurationStore.getInstance().getPollInterval(); + + // align to next poll slot + if (nextPoll + 2 * 60 * 60 * 1000 < now) { // last event more than 2h old -> could be a calibration + nextPoll = System.currentTimeMillis() + 1000; + } else { + // align to poll interval + nextPoll += (((now - nextPoll) / pollInterval)) * pollInterval + + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS; + if (pumpStatusData.getBatteryPercentage() > 25) { + // poll every 5 min + nextPoll += pollInterval; + } else { + // if pump battery seems to be empty reduce polling to save battery (every 15 min) + //TODO add message & document it + nextPoll += ConfigurationStore.getInstance().getLowBatteryPollInterval(); + } + } + + return nextPoll; } @Override @@ -100,7 +139,11 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc super.onCreate(savedInstanceState); mRealm = Realm.getDefaultInstance(); - mNightscoutUploadService = new Intent(this, NightscoutUploadIntentService.class); + + RealmResults<PumpStatusEvent> data = mRealm.where(PumpStatusEvent.class) + .findAllSorted("eventDate", Sort.DESCENDING); + if (data.size() > 0) + dataStore.setLastPumpStatus(data.first()); setContentView(R.layout.activity_main); @@ -111,13 +154,22 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc stopCgmService(); } + // setup preferences + configurationStore.setPollInterval(Long.parseLong(prefs.getString("pollInterval", Long.toString(MedtronicCnlIntentService.POLL_PERIOD_MS)))); + configurationStore.setLowBatteryPollInterval(Long.parseLong(prefs.getString("lowBatPollInterval", Long.toString(MedtronicCnlIntentService.LOW_BATTERY_POLL_PERIOD_MS)))); + configurationStore.setReducePollOnPumpAway(prefs.getBoolean("doublePollOnPumpAway", false)); + + chartZoom = Integer.parseInt(prefs.getString("chartZoom", "3")); + configurationStore.setMmolxl(prefs.getBoolean("mmolxl", false)); + configurationStore.setMmolxlDecimals(prefs.getBoolean("mmolDecimals", false)); + // 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 +189,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); @@ -175,22 +227,31 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc .withIcon(GoogleMaterial.Icon.gmd_settings) .withSelectable(false); final PrimaryDrawerItem itemRegisterUsb = new PrimaryDrawerItem() - .withName("Registered Devices") + .withName("Registered devices") .withIcon(GoogleMaterial.Icon.gmd_usb) .withSelectable(false); final PrimaryDrawerItem itemStopCollecting = new PrimaryDrawerItem() .withName("Stop collecting data") - .withIcon(GoogleMaterial.Icon.gmd_stop) + .withIcon(GoogleMaterial.Icon.gmd_power_settings_new) .withSelectable(false); final PrimaryDrawerItem itemGetNow = new PrimaryDrawerItem() .withName("Read data now") - .withIcon(GoogleMaterial.Icon.gmd_play_arrow) + .withIcon(GoogleMaterial.Icon.gmd_refresh) + .withSelectable(false); + final PrimaryDrawerItem itemUpdateProfile = new PrimaryDrawerItem() + .withName("Update pump profile") + .withIcon(GoogleMaterial.Icon.gmd_insert_chart) .withSelectable(false); final PrimaryDrawerItem itemClearLog = new PrimaryDrawerItem() - .withName("Clear Log") + .withName("Clear log") .withIcon(GoogleMaterial.Icon.gmd_clear_all) .withSelectable(false); + final PrimaryDrawerItem itemCheckForUpdate = new PrimaryDrawerItem() + .withName("Check for App update") + .withIcon(GoogleMaterial.Icon.gmd_update) + .withSelectable(false); + assert toolbar != null; new DrawerBuilder() .withActivity(this) .withAccountHeader(new AccountHeaderBuilder() @@ -204,10 +265,12 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc .withSelectedItem(-1) .addDrawerItems( itemSettings, + //itemUpdateProfile, // TODO - re-add when we to add Basal Profile Upload itemRegisterUsb, - itemStopCollecting, + itemCheckForUpdate, + itemClearLog, itemGetNow, - itemClearLog + itemStopCollecting ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override @@ -221,9 +284,12 @@ 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(); + } else if (drawerItem.equals(itemCheckForUpdate)) { + checkForUpdateNow(); } return false; @@ -232,7 +298,66 @@ 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); + +// due to bug in GraphView v4.2.1 using setNumHorizontalLabels reverted to using v4.0.1 and setOnXAxisBoundsChangedListener is n/a in this version +/* + 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); + +// due to bug in GraphView v4.2.1 using setNumHorizontalLabels reverted to using v4.0.1 and setHumanRounding is n/a in this version +// mChart.getGridLabelRenderer().setHumanRounding(false); + + mChart.getGridLabelRenderer().setLabelFormatter( + new DefaultLabelFormatter() { + DateFormat mFormat = new SimpleDateFormat("HH:mm", Locale.US); // 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 MainActivity.strFormatSGV(value); + } + } + } + ); + } + + @Override + protected void onStart() { + super.onStart(); + checkForUpdateBackground(5); } @Override @@ -247,7 +372,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 @@ -262,31 +387,14 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_menu_status: - Intent intent = new Intent(this, StatusActivity.class); - startActivity(intent); + // TODO - remove when we want to re-add the status menu item + //Intent intent = new Intent(this, StatusActivity.class); + //startActivity(intent); break; } 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); @@ -318,7 +426,22 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc private void clearLogText() { statusMessageReceiver.clearMessages(); - //mTextViewLog.setText("", BufferType.EDITABLE); + } + + private void checkForUpdateNow() { + new AppUpdater(this) + .setUpdateFrom(UpdateFrom.JSON) + .setUpdateJSON("https://raw.githubusercontent.com/pazaan/600SeriesAndroidUploader/master/app/update.json") + .showAppUpdated(true) // Show a dialog, even if there isn't an update + .start(); + } + + private void checkForUpdateBackground(int checkEvery) { + new AppUpdater(this) + .setUpdateFrom(UpdateFrom.JSON) + .setUpdateJSON("https://raw.githubusercontent.com/pazaan/600SeriesAndroidUploader/master/app/update.json") + .showEvery(checkEvery) // Only check for an update every `checkEvery` invocations + .start(); } private void startDisplayRefreshLoop() { @@ -330,7 +453,19 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } private void startCgmService() { - startCgmService(System.currentTimeMillis() + 1000); + startCgmServiceDelayed(0); + } + + private void startCgmServiceDelayed(long delay) { + if (!mRealm.isClosed()) { + RealmResults<PumpStatusEvent> results = mRealm.where(PumpStatusEvent.class) + .findAllSorted("eventDate", Sort.DESCENDING); + if (results.size() > 0) { + startCgmService(getNextPoll(results.first()) + delay); + return; + } + } + startCgmService(System.currentTimeMillis() + (delay == 0 ? 1000 : delay)); } private void startCgmService(long initialPoll) { @@ -340,25 +475,17 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc return; } - //clearLogText(); - // Cancel any existing polling. stopCgmService(); - medtronicCnlAlarmReceiver.setAlarm(initialPoll); - } - - private void uploadCgmData() { - startService(mNightscoutUploadService); + MedtronicCnlAlarmManager.setAlarm(initialPoll); } private void stopCgmService() { Log.i(TAG, "stopCgmService called"); - medtronicCnlAlarmReceiver.cancelAlarm(); + MedtronicCnlAlarmManager.cancelAlarm(); } private void showDisconnectionNotification(String title, String message) { - int notifyId = 1; - NotificationCompat.Builder mBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder(this) .setPriority(NotificationCompat.PRIORITY_MAX) @@ -383,8 +510,12 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc mBuilder.setContentIntent(resultPendingIntent); NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - // notifyId allows you to update the notification later on. - mNotificationManager.notify(notifyId, mBuilder.build()); + mNotificationManager.notify(USB_DISCONNECT_NOFICATION_ID, mBuilder.build()); + } + + private void clearDisconnectionNotification() { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(MainActivity.USB_DISCONNECT_NOFICATION_ID); } @Override @@ -413,7 +544,21 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc mEnableCgmService = true; startCgmService(); } - } else if (key.equals("mmolxl")) { + } else if (key.equals("mmolxl") || key.equals("mmolDecimals")) { + configurationStore.setMmolxl(sharedPreferences.getBoolean("mmolxl", false)); + configurationStore.setMmolxlDecimals(sharedPreferences.getBoolean("mmolDecimals", false)); + refreshDisplay(); + } else if (key.equals("pollInterval")) { + configurationStore.setPollInterval(Long.parseLong(sharedPreferences.getString("pollInterval", + Long.toString(MedtronicCnlIntentService.POLL_PERIOD_MS)))); + } else if (key.equals("lowBatPollInterval")) { + configurationStore.setLowBatteryPollInterval(Long.parseLong(sharedPreferences.getString("lowBatPollInterval", + Long.toString(MedtronicCnlIntentService.LOW_BATTERY_POLL_PERIOD_MS)))); + } else if (key.equals("doublePollOnPumpAway")) { + configurationStore.setReducePollOnPumpAway(sharedPreferences.getBoolean("doublePollOnPumpAway", false)); + } else if (key.equals("chartZoom")) { + chartZoom = Integer.parseInt(sharedPreferences.getString("chartZoom", "3")); + hasZoomedChart = false; refreshDisplay(); } } @@ -434,77 +579,118 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } public void openUsbRegistration() { - if (hasDetectedCnl()) { - Intent loginIntent = new Intent(this, GetHmacAndKeyActivity.class); - startActivity(loginIntent); - } - } - - private String renderTrendHtml(PumpStatusEvent.CGM_TREND trend) { - switch (trend) { - case DOUBLE_UP: - return "⇈"; - case SINGLE_UP: - return "↑"; - case FOURTY_FIVE_UP: - return "↗"; - case FLAT: - return "→"; - case FOURTY_FIVE_DOWN: - return "↘"; - case SINGLE_DOWN: - return "↓"; - case DOUBLE_DOWN: - return "⇊"; - default: - return "—"; - } + Intent manageCNLIntent = new Intent(this, ManageCNLActivity.class); + startActivity(manageCNLIntent); } private PumpInfo getActivePump() { + long activePumpMac = dataStore.getActivePumpMac(); if (activePumpMac != 0L && (mActivePump == null || !mActivePump.isValid() || mActivePump.getPumpMac() != activePumpMac)) { - mActivePump = null; + if (mActivePump != null) { + // remove listener on old pump + mActivePump.removeAllChangeListeners(); + mActivePump = null; + } PumpInfo pump = mRealm .where(PumpInfo.class) - .equalTo("pumpMac", MainActivity.activePumpMac) + .equalTo("pumpMac", activePumpMac) .findFirst(); 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(); + } + }); } } return mActivePump; } + + public static String strFormatSGV(double sgvValue) { + ConfigurationStore configurationStore = ConfigurationStore.getInstance(); + + NumberFormat sgvFormatter; + if (configurationStore.isMmolxl()) { + if (configurationStore.isMmolxlDecimals()) { + sgvFormatter = new DecimalFormat("0.00"); + } else { + sgvFormatter = new DecimalFormat("0.0"); + } + return sgvFormatter.format(sgvValue / MMOLXLFACTOR); + } else { + sgvFormatter = new DecimalFormat("0"); + return sgvFormatter.format(sgvValue); + } + } + + public static String renderTrendSymbol(PumpStatusEvent.CGM_TREND trend) { + switch (trend) { + case DOUBLE_UP: + return "\u21c8"; + case SINGLE_UP: + return "\u2191"; + case FOURTY_FIVE_UP: + return "\u2197"; + case FLAT: + return "\u2192"; + case FOURTY_FIVE_DOWN: + return "\u2198"; + case SINGLE_DOWN: + return "\u2193"; + case DOUBLE_DOWN: + return "\u21ca"; + default: + return "\u2014"; + } + } + private class StatusMessageReceiver extends BroadcastReceiver { private class StatusMessage { private long timestamp; private String message; - public StatusMessage(String message) { + StatusMessage(String message) { this(System.currentTimeMillis(), message); } - public StatusMessage(long timestamp, String message) { - this.timestamp = timestamp; - this.message = message; - } - - public long getTimestamp() { - return timestamp; - } - - public void setTimestamp(long timestamp) { + StatusMessage(long timestamp, String message) { this.timestamp = timestamp; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { this.message = message; } @@ -513,7 +699,7 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } } - private Queue<StatusMessage> messages = new ArrayBlockingQueue<>(10); + private final Queue<StatusMessage> messages = new ArrayBlockingQueue<>(400); @Override public void onReceive(Context context, Intent intent) { @@ -521,7 +707,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)); @@ -549,11 +735,10 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc private class RefreshDisplayRunnable implements Runnable { @Override public void run() { - // UI elements - TODO do these need to be members? 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 (configurationStore.isMmolxl()) { textViewUnits.setText(R.string.text_unit_mmolxl); } else { textViewUnits.setText(R.string.text_unit_mgxdl); @@ -564,130 +749,180 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc // Get the most recently written CGM record for the active pump. PumpStatusEvent pumpStatusData = null; - PumpInfo pump = getActivePump(); - - if (pump != null && pump.isValid()) { - pumpStatusData = pump.getPumpHistory().last(); + if (dataStore.getLastPumpStatus().getEventDate().getTime() > 0) { + pumpStatusData = dataStore.getLastPumpStatus(); } - // FIXME - grab the last item from the activePump's getPumpHistory - RealmResults<PumpStatusEvent> results = - mRealm.where(PumpStatusEvent.class) - .findAllSorted("eventDate", Sort.ASCENDING); - - if (pumpStatusData == null) { - return; - } - - DecimalFormat df; - if (prefs.getBoolean("mmolDecimals", false)) - df = new DecimalFormat("0.00"); - else - df = new DecimalFormat("0.0"); + updateChart(mRealm.where(PumpStatusEvent.class) + .greaterThan("sgvDate", new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24)) + .findAllSorted("sgvDate", Sort.ASCENDING)); + + if (pumpStatusData != null) { + String sgvString; + if (pumpStatusData.isCgmActive()) { + sgvString = MainActivity.strFormatSGV(pumpStatusData.getSgv()); + if (configurationStore.isMmolxl()) { + Log.d(TAG, sgvString + " mmol/L"); + } else { + Log.d(TAG, sgvString + " mg/dL"); + } + } else { + sgvString = "\u2014"; // — + } - 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.getSgvDate().getTime())); + + textViewTrend.setText(MainActivity.renderTrendSymbol(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); + mChart.getGridLabelRenderer().setNumHorizontalLabels(6); - ArrayList<ILineDataSet> dataSets = new ArrayList<ILineDataSet>(); - dataSets.add(lineDataSet); + int size = results.size(); + if (size == 0) { + final long now = System.currentTimeMillis(), + left = now - chartZoom * 60 * 60 * 1000; - LineData lineData = new LineData(dataSets); + mChart.getViewport().setXAxisBoundsManual(true); + mChart.getViewport().setMaxX(now); + mChart.getViewport().setMinX(left); - // set data - mChart.setMinimumHeight(200); - mChart.setData(lineData); - } - } - - private class RefreshDataReceiver extends BroadcastReceiver { + mChart.getViewport().setYAxisBoundsManual(true); + mChart.getViewport().setMinY(80); + mChart.getViewport().setMaxY(120); - @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.postInvalidate(); return; } - PumpStatusEvent pumpStatusData = null; - - PumpInfo pump = getActivePump(); + DataPoint[] entries = new DataPoint[size]; - if (pump != null && pump.isValid()) { - pumpStatusData = pump.getPumpHistory().last(); - } else { - return; + int pos = 0; + for (PumpStatusEvent pumpStatus : results) { + // turn your data into Entry objects + entries[pos++] = new DataPoint(pumpStatus.getSgvDate(), (double) pumpStatus.getSgv()); } - long nextPoll = pumpStatusData.getEventDate().getTime() + pumpStatusData.getPumpTimeOffset() - + MedtronicCnlIntentService.POLL_GRACE_PERIOD_MS + MedtronicCnlIntentService.POLL_PERIOD_MS; - startCgmService(nextPoll); + 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); - // 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(); + PointsGraphSeries sgvSerie = new PointsGraphSeries(entries); +// sgvSerie.setSize(3.6f); +// sgvSerie.setColor(Color.LTGRAY); - if (results.size() > 0) { - mRealm.executeTransaction(new Realm.Transaction() { + + sgvSerie.setOnDataPointTapListener(new OnDataPointTapListener() { + DateFormat mFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM); + + @Override + public void onTap(Series series, DataPointInterface dataPoint) { + double sgv = dataPoint.getY(); + + StringBuilder sb = new StringBuilder(mFormat.format(new Date((long) dataPoint.getX())) + ": "); + sb.append(MainActivity.strFormatSGV(sgv)); + Toast.makeText(getBaseContext(), sb.toString(), Toast.LENGTH_SHORT).show(); + } + }); + + sgvSerie.setCustomShape(new PointsGraphSeries.CustomShape() { @Override - public void execute(Realm realm) { - // Delete all matches - Log.d(TAG, "Deleting " + results.size() + " records from realm"); - results.deleteAllFromRealm(); + public void draw(Canvas canvas, Paint paint, float x, float y, DataPointInterface dataPoint) { + double sgv = dataPoint.getY(); + if (sgv < 80) + paint.setColor(Color.RED); + else if (sgv <= 180) + paint.setColor(Color.GREEN); + else if (sgv <= 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 viewport 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(); } } private class UsbReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { + // TODO move this somewhere else ... wherever it belongs + // realm might be closed ... sometimes occurs when USB is disconnected and replugged ... + if (mRealm.isClosed()) mRealm = Realm.getDefaultInstance(); String action = intent.getAction(); if (MedtronicCnlIntentService.Constants.ACTION_USB_PERMISSION.equals(action)) { boolean permissionGranted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); @@ -700,10 +935,13 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc } } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { Log.d(TAG, "USB plugged in"); + if (mEnableCgmService) { + clearDisconnectionNotification(); + } 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(); @@ -728,8 +966,9 @@ public class MainActivity extends AppCompatActivity implements OnSharedPreferenc if (arg1.getAction().equalsIgnoreCase(Intent.ACTION_BATTERY_LOW) || arg1.getAction().equalsIgnoreCase(Intent.ACTION_BATTERY_CHANGED) || arg1.getAction().equalsIgnoreCase(Intent.ACTION_BATTERY_OKAY)) { - batLevel = arg1.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); + dataStore.setUploaderBatteryLevel(arg1.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)); } } } + } 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..78d55479c0627ff3ebf470d7e5e01700bfe6dfe4 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/ManageCNLActivity.java @@ -0,0 +1,154 @@ +package info.nightscout.android.medtronic; + +import android.content.Context; +import android.graphics.Color; +import android.os.Bundle; +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<>(); + + 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.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)); + lView.setAdapter(adapter); + } + + @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, parent, false); + } + + //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..9c2080c5b70aab426520174927c1b0a67b6b9cfe 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(); - } - - if (message instanceof MedtronicMessage) { - mPumpSession.incrMedtronicSequenceNumber(); - } - } - - @Override - public ContourNextLinkMessage receiveMessage() { - return null; - } - - public void sendMessage(byte[] message) throws IOException { + public void requestDeviceInfo() + throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { + DeviceInfoResponseCommandMessage response = new DeviceInfoRequestCommandMessage().send(mDevice); - 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 - extract more details form the device info. + mStickSerial = response.getSerial(); } - // 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; + public void enterControlMode() throws IOException, TimeoutException, UnexpectedMessageException, ChecksumException, EncryptionException { + boolean doRetry; - // 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(); + RequestLinkKeyResponseMessage response = new RequestLinkKeyRequestMessage(mPumpSession).send(mDevice); + this.getPumpSession().setKey(response.getKey()); - this.getPumpSession().setPackedLinkKey(packedLinkKey); - - Log.d(TAG, String.format("Finished requestLinkKey. linkKey = '%s'", this.getPumpSession().getKey())); + Log.d(TAG, String.format("Finished requestLinkKey. linkKey = '%s'", (Object) 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) { @@ -293,6 +141,7 @@ public class MedtronicCnlReader implements ContourNextLinkMessageHandler { if (lastChannel != null) { radioChannels.add(0, lastChannel); + radioChannels.add(5, lastChannel); // retry last used channel again, this allows for transient noise if missed on first attempt when pump is in range } } @@ -300,26 +149,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 +164,77 @@ 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()); + PumpTimeResponseMessage response = new PumpTimeRequestMessage(mPumpSession).send(mDevice); - 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(); - } - - // 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); - - 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) + public PumpStatusEvent updatePumpStatus(PumpStatusEvent pumpRecord) throws IOException, EncryptionException, ChecksumException, TimeoutException, UnexpectedMessageException { + Log.d(TAG, "Begin updatePumpStatus"); - new PumpStatusRequestMessage(mPumpSession).send(this); - // Read the 0x81 - readMessage(); + PumpStatusResponseMessage response = new PumpStatusRequestMessage(mPumpSession).send(mDevice); + response.updatePumpRecord(pumpRecord); - // 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; - } - - // 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))); - } - // 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); - - // Recent Bolus Wizard BGL - pumpRecord.setRecentBolusWizard(statusBuffer.get(0x48) != 0); - pumpRecord.setBolusWizardBGL(statusBuffer.getShort(0x49) & 0x0000ffff); // In mg/DL - - Log.d(TAG, "Finished getPumpStatus"); + Log.d(TAG, "Finished updatePumpStatus"); + 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); + + Log.d(TAG, "Finished getBasalPatterns"); + } - // Read the 0x80 - ContourNextLinkMessage response = PumpBasalPatternResponseMessage.fromBytes(mPumpSession, readMessage()); - // 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; - } - */ + 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) - // FIXME - this needs to go into PumpBasalPatternResponseMessage - ByteBuffer basalRatesBuffer = ByteBuffer.allocate(96); - basalRatesBuffer.order(ByteOrder.BIG_ENDIAN); - basalRatesBuffer.put(response.encode(), 0x39, 96); + ReadHistoryInfoResponseMessage response = new ReadHistoryInfoRequestMessage(mPumpSession).send(mDevice); - Log.d(TAG, "Finished getBasalPatterns"); + 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..918e794d9e3388d3c565561f1e9a77af519c38cf 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,12 +19,10 @@ public class MedtronicCnlSession { private long pumpMAC; private byte radioChannel; - private int bayerSequenceNumber = 1; - private int medtronicSequenceNumber = 1; + private byte radioRSSI; - /*public byte[] getHMAC() { - return HMAC; - }*/ + private int cnlSequenceNumber = 1; + private int medtronicSequenceNumber = 1; public byte[] getHMAC() throws NoSuchAlgorithmException { String shortSerial = this.stickSerial.replaceAll("\\d+-", ""); @@ -68,8 +65,8 @@ public class MedtronicCnlSession { this.pumpMAC = pumpMAC; } - public int getBayerSequenceNumber() { - return bayerSequenceNumber; + public int getCnlSequenceNumber() { + return cnlSequenceNumber; } public int getMedtronicSequenceNumber() { @@ -80,8 +77,16 @@ public class MedtronicCnlSession { return radioChannel; } - public void incrBayerSequenceNumber() { - bayerSequenceNumber++; + public byte getRadioRSSI() { + return radioRSSI; + } + + public int getRadioRSSIpercentage() { + return (((int) radioRSSI & 0x00FF) * 100) / 0xA8; + } + + public void incrCnlSequenceNumber() { + cnlSequenceNumber++; } public void incrMedtronicSequenceNumber() { @@ -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..7a2d11bb6045345401faefb8eca06641ce6faae7 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,14 @@ 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 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..abf23486fbd627c1bd271301f14584f17a507cb9 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ChannelNegotiateRequestMessage.java @@ -0,0 +1,57 @@ +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"); + + return this.getResponse(readMessage(mDevice)); + } + + @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..711728a5978a6e1938adc3b8719fcf29f4200e0d --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/CloseConnectionRequestMessage.java @@ -0,0 +1,42 @@ +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 10.12.2016. + */ + +public class CloseConnectionRequestMessage extends ContourNextLinkBinaryRequestMessage<CloseConnectionResponseMessage> { + public CloseConnectionRequestMessage(MedtronicCnlSession pumpSession, byte[] payload) throws ChecksumException { + super(CommandType.CLOSE_CONNECTION, pumpSession, payload); + } + + @Override + public CloseConnectionResponseMessage send(UsbHidDriver mDevice, int millis) throws IOException, TimeoutException, ChecksumException, EncryptionException, UnexpectedMessageException { + + // clear unexpected incoming messages + clearMessage(mDevice); + + sendMessage(mDevice); + if (millis > 0) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + + return this.getResponse(readMessage(mDevice)); + } + + @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..7f189e33fd89431d2b4bd6c89269fe1b12801698 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/CloseConnectionResponseMessage.java @@ -0,0 +1,14 @@ +package info.nightscout.android.medtronic.message; + +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 55% 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..fadfec17187f3a7ade746ea7d48473471a8208e8 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.incrCnlSequenceNumber(); } protected static byte[] buildPayload(CommandType commandType, MedtronicCnlSession pumpSession, byte[] payload) { @@ -52,11 +54,11 @@ public class ContourNextLinkBinaryMessage extends ContourNextLinkMessage{ payloadBuffer.put((byte) 0x51); payloadBuffer.put((byte) 0x3); - payloadBuffer.put("000000".getBytes()); // Text of PumpInfo serial, but 000000 for 640g + payloadBuffer.put("000000".getBytes()); // Text of PumpInfo serial, but 000000 for 600 Series pumps byte[] unknownBytes = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; payloadBuffer.put(unknownBytes); - payloadBuffer.put(commandType.value); - payloadBuffer.putInt(pumpSession.getBayerSequenceNumber()); + payloadBuffer.put(commandType.getValue()); + payloadBuffer.putInt(pumpSession.getCnlSequenceNumber()); byte[] unknownBytes2 = {0, 0, 0, 0, 0}; payloadBuffer.put(unknownBytes2); payloadBuffer.putInt(payloadLength); @@ -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..7a948f02fe07a27c2ef6c386ae62a172686a1197 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,15 @@ package info.nightscout.android.medtronic.message; +import info.nightscout.android.medtronic.exception.ChecksumException; + /** * 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 +17,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..0cbe120e2b998dd89680d16f0c5087b6f53e5dc8 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,213 @@ 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.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 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(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(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..681bd909cc388bfe2671bd809d7e3f18c0cbfd95 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkRequestMessage.java @@ -0,0 +1,45 @@ +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) { + } + } + + // FIXME - We need to care what the response message is - wrong MAC and all that + return this.getResponse(readMessage(mDevice)); + } + + 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..614a2c4f33dba239941183fda7557d21932353b8 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ContourNextLinkResponseMessage.java @@ -0,0 +1,28 @@ +package info.nightscout.android.medtronic.message; + +import java.util.Locale; + +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..bb681200de437f12f126839a11379b2e130b3c3e 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,14 @@ 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 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 +16,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/MessageUtils.java b/app/src/main/java/info/nightscout/android/medtronic/message/MessageUtils.java index 5b800808e7ab294f9f115f66589d971ac8dffa32..f5a80e07d2fb8bb3263f92683155c82affc48241 100644 --- a/app/src/main/java/info/nightscout/android/medtronic/message/MessageUtils.java +++ b/app/src/main/java/info/nightscout/android/medtronic/message/MessageUtils.java @@ -66,7 +66,6 @@ public class MessageUtils { // However, the time the pump *means* is Fri, 13 May 2016 21:07:48 in our own timezone long offsetFromUTC = currentTz.getOffset(Calendar.getInstance().getTimeInMillis()); - Date pumpDate = new Date((( baseTime + rtc + offset ) * 1000 ) - offsetFromUTC ); - return pumpDate; + return new Date((( baseTime + rtc + offset ) * 1000 ) - offsetFromUTC ); } } 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..808324743c26b2f71255e00bd48c59b1247356d5 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionRequestMessage.java @@ -0,0 +1,20 @@ +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 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..d20c421e301c92e6903720b03b51badc367d45d2 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/OpenConnectionResponseMessage.java @@ -0,0 +1,14 @@ +package info.nightscout.android.medtronic.message; + +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..18be6188ba743957c83cfd328d4e5d5e0e276033 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,22 @@ package info.nightscout.android.medtronic.message; +import java.io.IOException; + 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..6fbe8768dadc0ece6003a59b3cdad7dcc88d32a6 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,56 @@ 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) { + } + } + // Read the 0x80 + byte[] payload = readMessage(mDevice); + + // clear unexpected incoming messages + clearMessage(mDevice); + + return this.getResponse(payload); + } + + @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..6a70ee7ed1b6d3248102202e43f49d88e26bca86 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 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.DataStore; +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. + + 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 + long rtc; + long offset; + if ((sgv & 0x200) == 0x200) { + // Sensor error. Let's reset. FIXME - solve this more elegantly later + sgv = 0; + rtc = 0; + offset = 0; + 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 + 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 data + pumpRecord.setSgv(sgv); + pumpRecord.setSgvDate(new Date(sgvDate.getTime() - pumpRecord.getPumpTimeOffset())); + pumpRecord.setCgmTrend(cgmTrend); - // 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); + // there is a BolusWizard usage & the IOB increased + if (activeInsulin > DataStore.getInstance().getLastPumpStatus().getActiveInsulin()) { + pumpRecord.setBolusWizardBGL(bolusWizardBGL); // In mg/DL + } else { + pumpRecord.setBolusWizardBGL(0); // 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..7332a48e0c88c609cd9f2db4a7687b6f6c6195d3 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 + byte[] payload = readMessage(mDevice); + + // Pump sends additional 0x80 message when not using EHSM, lets clear this and any unexpected incoming messages + clearMessage(mDevice); + + return this.getResponse(payload); + } + + @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..c64ba271f7cf9b10a53fa5f125f980b0b062423a --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ReadHistoryInfoRequestMessage.java @@ -0,0 +1,35 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; + +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..01c63ce23d0263e2ba3ff4ed30f313eb441362ea --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/ReadInfoRequestMessage.java @@ -0,0 +1,22 @@ +package info.nightscout.android.medtronic.message; + +import java.io.IOException; + +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..1814fe87fa8df4322fdeead228477434986ca466 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/message/RequestLinkKeyRequestMessage.java @@ -0,0 +1,20 @@ +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 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..b58d702ce647bf2c1014af714954c15107661788 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/medtronic/service/MedtronicCnlAlarmManager.java @@ -0,0 +1,81 @@ +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.util.Log; + +import java.util.Date; + +import info.nightscout.android.utils.ConfigurationStore; + +/** + * 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; + + private static PendingIntent pendingIntent = null; + private static AlarmManager alarmManager = null; + + 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); + } + + /** + * 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 onReceive + 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; + + cancelAlarm(); + + 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 and 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(ConfigurationStore.getInstance().getPollInterval()); // 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..ad908713062d88a4c902ab0f933e69354ffd88fe 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,10 +1,7 @@ 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; @@ -17,8 +14,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 +24,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(); + MedtronicCnlAlarmManager.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); - } - - // 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..1ef2deaa0780b4b08c679af3365a0c1009c7cf32 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,19 +20,23 @@ 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; import info.nightscout.android.upload.nightscout.NightscoutUploadReceiver; +import info.nightscout.android.utils.ConfigurationStore; +import info.nightscout.android.utils.DataStore; import info.nightscout.android.xdrip_plus.XDripPlusUploadReceiver; import io.realm.Realm; import io.realm.RealmResults; @@ -42,13 +46,19 @@ 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(); + private UsbHidDriver mHidDevice; private Context mContext; private NotificationManagerCompat nm; private UsbManager mUsbManager; + private DataStore dataStore = DataStore.getInstance(); + private ConfigurationStore configurationStore = ConfigurationStore.getInstance(); + private DateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss", Locale.US); + public MedtronicCnlIntentService() { super(MedtronicCnlIntentService.class.getName()); @@ -96,195 +106,284 @@ public class MedtronicCnlIntentService extends IntentService { protected void onHandleIntent(Intent intent) { Log.d(TAG, "onHandleIntent called"); + try { + // TODO use of ConfigurationStore is confusinng if pollInterval uses the CS, which + // uses the POLL_PERIOD_MS, while the latter constant is also used directly. + + // Note that the variable pollInterval refers to the poll we'd like to make to the pump, + // based on settings and battery level, while POLL_PERIOD_MS is used to calculate + // when the pump is going to poll data from the transmitter again. + // Thus POLL_PERIOD_MS is important to calculate times we'd be clashing with transmitter + // to pump transmissions, which are then checked against the time the uploader would + // like to poll, which is calculated using the pollInterval variable. + // TODO find better variable names to make this distinction clearer and/or if possible + // do more method extraction refactorings to make this method easier to grasp + + final long timePollStarted = System.currentTimeMillis(); + final long timeLastGoodSGV = dataStore.getLastPumpStatus().getSgvDate().getTime(); + + final long timePollExpected; + if (timeLastGoodSGV != 0) { + timePollExpected = timeLastGoodSGV + POLL_PERIOD_MS + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((timePollStarted - 1000L - (timeLastGoodSGV + POLL_GRACE_PERIOD_MS)) / POLL_PERIOD_MS)); + } else { + timePollExpected = timePollStarted; + } - if (!hasUsbHostFeature()) { - sendStatus("It appears that this device doesn't support USB OTG."); - Log.e(TAG, "Device does not support USB OTG"); - MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); - // TODO - throw, don't return - return; - } + // 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); + return; + } - UsbDevice cnlStick = UsbHidDriver.getUsbDevice(mUsbManager, USB_VID, USB_PID); - if (cnlStick == null) { - sendStatus("USB connection error. Is the Bayer Contour Next Link plugged in?"); - Log.w(TAG, "USB connection error. Is the CNL plugged in?"); + final short pumpBatteryLevel = dataStore.getLastPumpStatus().getBatteryPercentage(); + long pollInterval = configurationStore.getPollInterval(); + if ((pumpBatteryLevel > 0) && (pumpBatteryLevel <= 25)) { + pollInterval = configurationStore.getLowBatteryPollInterval(); + } - // TODO - set status if offline or Nightscout not reachable - uploadToNightscout(); - MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); // TODO - throw, don't return - return; - } + if (!openUsbDevice()) + return; - if (!mUsbManager.hasPermission(UsbHidDriver.getUsbDevice(mUsbManager, USB_VID, USB_PID))) { - sendMessage(Constants.ACTION_NO_USB_PERMISSION); - MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); - // TODO - throw, don't return - return; - } - mHidDevice = UsbHidDriver.acquire(mUsbManager, cnlStick); + MedtronicCnlReader cnlReader = new MedtronicCnlReader(mHidDevice); - try { - mHidDevice.open(); - } catch (Exception e) { - Log.e(TAG, "Unable to open serial device", e); - MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); - // TODO - throw, don't return - return; - } + Realm realm = Realm.getDefaultInstance(); + realm.beginTransaction(); - MedtronicCnlReader cnlReader = new MedtronicCnlReader(mHidDevice); + try { + sendStatus("Connecting to Contour Next Link"); + Log.d(TAG, "Connecting to Contour Next Link"); + cnlReader.requestDeviceInfo(); + + // Is the device already configured? + ContourNextLinkInfo info = realm + .where(ContourNextLinkInfo.class) + .equalTo("serialNumber", cnlReader.getStickSerial()) + .findFirst(); - Realm realm = Realm.getDefaultInstance(); - realm.beginTransaction(); + if (info == null) { + info = realm.createObject(ContourNextLinkInfo.class, cnlReader.getStickSerial()); + } - try { - sendStatus("Connecting to the Contour Next Link..."); - Log.d(TAG, "Connecting to the Contour Next Link."); - cnlReader.requestDeviceInfo(); - - // Is the device already configured? - ContourNextLinkInfo info = realm - .where(ContourNextLinkInfo.class) - .equalTo("serialNumber", cnlReader.getStickSerial()) - .findFirst(); - - if (info == null) { - // TODO - use realm.createObject()? - info = new ContourNextLinkInfo(); - info.setSerialNumber(cnlReader.getStickSerial()); - - info = realm.copyToRealm(info); - } + cnlReader.getPumpSession().setStickSerial(info.getSerialNumber()); - cnlReader.getPumpSession().setStickSerial(info.getSerialNumber()); + cnlReader.enterControlMode(); - cnlReader.enterControlMode(); + try { + cnlReader.enterPassthroughMode(); + cnlReader.openConnection(); - try { - cnlReader.enterPassthroughMode(); - cnlReader.openConnection(); - cnlReader.requestReadInfo(); + cnlReader.requestReadInfo(); - String key = info.getKey(); + String key = info.getKey(); - if (key == null) { - cnlReader.requestLinkKey(); + if (key == null) { + cnlReader.requestLinkKey(); - info.setKey(MessageUtils.byteArrayToHexString(cnlReader.getPumpSession().getKey())); - key = info.getKey(); - } + info.setKey(MessageUtils.byteArrayToHexString(cnlReader.getPumpSession().getKey())); + key = info.getKey(); + } - cnlReader.getPumpSession().setKey(MessageUtils.hexStringToByteArray(key)); + cnlReader.getPumpSession().setKey(MessageUtils.hexStringToByteArray(key)); - 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(); + long pumpMAC = cnlReader.getPumpSession().getPumpMAC(); + Log.i(TAG, "PumpInfo MAC: " + (pumpMAC & 0xffffff)); + PumpInfo activePump = realm + .where(PumpInfo.class) + .equalTo("pumpMac", pumpMAC) + .findFirst(); - if (activePump == null) { - activePump = realm.createObject(PumpInfo.class); - activePump.setPumpMac(pumpMAC); - } + if (activePump == null) { + activePump = realm.createObject(PumpInfo.class, pumpMAC); + } - 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?"); - } else { - activePump.setLastRadioChannel(radioChannel); - sendStatus(String.format(Locale.getDefault(), "Connected to Contour Next Link on channel %d.", (int) radioChannel)); - Log.d(TAG, String.format("Connected to Contour Next Link on channel %d.", (int) radioChannel)); - cnlReader.beginEHSMSession(); + activePump.updateLastQueryTS(); - PumpStatusEvent pumpRecord = realm.createObject(PumpStatusEvent.class); + byte radioChannel = cnlReader.negotiateChannel(activePump.getLastRadioChannel()); + if (radioChannel == 0) { + sendStatus("Could not communicate with the pump. Is it nearby?"); + Log.i(TAG, "Could not communicate with the pump. Is it nearby?"); + pollInterval = configurationStore.getPollInterval() / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L); // reduce polling interval to half until pump is available + } else if (cnlReader.getPumpSession().getRadioRSSIpercentage() < 5) { + sendStatus(String.format(Locale.getDefault(), "Connected on channel %d RSSI: %d%%", (int) radioChannel, cnlReader.getPumpSession().getRadioRSSIpercentage())); + sendStatus("Warning: pump signal too weak. Is it nearby?"); + Log.i(TAG, "Warning: pump signal too weak. Is it nearby?"); + pollInterval = configurationStore.getPollInterval() / (configurationStore.isReducePollOnPumpAway() ? 2L : 1L); // reduce polling interval to half until pump is available + } else { + dataStore.setActivePumpMac(pumpMAC); - String deviceName = String.format("medtronic-640g://%s", cnlReader.getStickSerial()); - activePump.setDeviceName(deviceName); + activePump.setLastRadioChannel(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)); - // TODO - this should not be necessary. We should reverse lookup the device name from PumpInfo - pumpRecord.setDeviceName(deviceName); + // read pump status + PumpStatusEvent pumpRecord = realm.createObject(PumpStatusEvent.class); - long pumpTime = cnlReader.getPumpTime().getTime(); - long pumpOffset = pumpTime - System.currentTimeMillis(); - Log.d(TAG, "Time offset between pump and device: " + pumpOffset + " millis."); + String deviceName = String.format("medtronic-600://%s", cnlReader.getStickSerial()); + activePump.setDeviceName(deviceName); - // 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); + // TODO - this should not be necessary. We should reverse lookup the device name from PumpInfo + pumpRecord.setDeviceName(deviceName); - cnlReader.endEHSMSession(); + long pumpTime = cnlReader.getPumpTime().getTime(); + long pumpOffset = pumpTime - System.currentTimeMillis(); + Log.d(TAG, "Time offset between pump and device: " + pumpOffset + " millis."); + + // TODO - send ACTION to MainActivity to show offset between pump and uploader. + pumpRecord.setPumpTimeOffset(pumpOffset); + pumpRecord.setPumpDate(new Date(pumpTime - pumpOffset)); + cnlReader.updatePumpStatus(pumpRecord); - boolean cancelTransaction = true; if (pumpRecord.getSgv() != 0) { + String offsetSign = ""; + if (pumpOffset > 0) { + offsetSign = "+"; + } + sendStatus("SGV: " + MainActivity.strFormatSGV(pumpRecord.getSgv()) + " At: " + dateFormatter.format(pumpRecord.getSgvDate().getTime()) + " Pump: " + offsetSign + (pumpOffset / 1000L) + "sec"); //note: event time is currently stored with offset + + // Check if pump sent old event when new expected + if (dataStore.getLastPumpStatus() != null && + dataStore.getLastPumpStatus().getSgvDate() != null && + pumpRecord.getSgvDate().getTime() - dataStore.getLastPumpStatus().getSgvDate().getTime() < 5000L && + timePollExpected - timePollStarted < 5000L) { + sendStatus("Pump sent old SGV event"); + } + + dataStore.clearUnavailableSGVCount(); // 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("sgvDate", pumpRecord.getSgvDate()) .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); + dataStore.setLastPumpStatus(pumpRecord); } + } else { + sendStatus("SGV: unavailable from pump"); + dataStore.incUnavailableSGVCount(); // poll clash detection + } + + realm.commitTransaction(); // Tell the Main Activity we have new data - sendMessage(Constants.ACTION_REFRESH_DATA); + sendMessage(Constants.ACTION_UPDATE_PUMP); } - if (cancelTransaction) { - realm.cancelTransaction(); + } catch (UnexpectedMessageException e) { + Log.e(TAG, "Unexpected Message", e); + sendStatus("Communication Error: " + e.getMessage()); + pollInterval = 60000L; // retry once during this poll period, this allows for transient radio noise + } catch (TimeoutException e) { + Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); + sendStatus("Timeout communicating with the Contour Next Link."); + pollInterval = 60000L; // retry once during this poll period, this allows for transient radio noise + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "Could not determine CNL HMAC", e); + sendStatus("Error connecting to Contour Next Link: Hashing error."); + } finally { + try { + cnlReader.closeConnection(); + cnlReader.endPassthroughMode(); + cnlReader.endControlMode(); + } catch (NoSuchAlgorithmException e) { } + } + } catch (IOException e) { + Log.e(TAG, "Error connecting to Contour Next Link.", e); + sendStatus("Error connecting to Contour Next Link."); + } catch (ChecksumException e) { + Log.e(TAG, "Checksum error getting message from the Contour Next Link.", e); + sendStatus("Checksum error getting message from the Contour Next Link."); + } catch (EncryptionException e) { + Log.e(TAG, "Error decrypting messages from Contour Next Link.", e); + sendStatus("Error decrypting messages from Contour Next Link."); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); + sendStatus("Timeout communicating with the Contour Next Link."); } catch (UnexpectedMessageException e) { - Log.e(TAG, "Unexpected Message", e); - sendStatus("Communication Error: " + e.getMessage()); - } catch (NoSuchAlgorithmException e) { - Log.e(TAG, "Could not determine CNL HMAC", e); - sendStatus("Error connecting to Contour Next Link: Hashing error."); + Log.e(TAG, "Could not close connection.", e); + sendStatus("Could not close connection: " + e.getMessage()); } finally { - //TODO : 05.11.2016 has the close to be here? - cnlReader.closeConnection(); - cnlReader.endPassthroughMode(); - cnlReader.endControlMode(); + if (!realm.isClosed()) { + if (realm.isInTransaction()) { + // If we didn't commit the transaction, we've run into an error. Let's roll it back + realm.cancelTransaction(); + } + realm.close(); + } + + uploadPollResults(); + scheduleNextPoll(timePollStarted, timeLastGoodSGV, pollInterval); } - } catch (IOException e) { - Log.e(TAG, "Error connecting to Contour Next Link.", e); - sendStatus("Error connecting to Contour Next Link."); - } catch (ChecksumException e) { - Log.e(TAG, "Checksum error getting message from the Contour Next Link.", e); - sendStatus("Checksum error getting message from the Contour Next Link."); - } catch (EncryptionException e) { - Log.e(TAG, "Error decrypting messages from Contour Next Link.", e); - sendStatus("Error decrypting messages from Contour Next Link."); - } catch (TimeoutException e) { - Log.e(TAG, "Timeout communicating with the Contour Next Link.", e); - sendStatus("Timeout communicating with the Contour Next Link."); - } catch (UnexpectedMessageException e) { - Log.e(TAG, "Could not close connection.", e); - sendStatus("Could not close connection: " + e.getMessage()); } finally { - if (!realm.isClosed()) { - if (realm.isInTransaction()) { - // If we didn't commit the transaction, we've run into an error. Let's roll it back - realm.cancelTransaction(); - } - realm.close(); + MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); + } + } + + private void scheduleNextPoll(long timePollStarted, long timeLastGoodSGV, long pollInterval) { + // smart polling and pump-sensor poll clash detection + long lastActualPollTime = timePollStarted; + if (timeLastGoodSGV > 0) { + lastActualPollTime = timeLastGoodSGV + POLL_GRACE_PERIOD_MS + (POLL_PERIOD_MS * ((System.currentTimeMillis() - (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 (dataStore.getUnavailableSGVCount() > 0) { + if (timeLastGoodSGV == 0) { + nextRequestedPollTime += POLL_PERIOD_MS / 5L; // if there is a uploader/sensor poll clash on startup then this will push the next attempt out by 60 seconds + } else if (dataStore.getUnavailableSGVCount() > 2) { + sendStatus("Warning: No SGV available from pump for " + dataStore.getUnavailableSGVCount() + " attempts"); + nextRequestedPollTime += ((long) ((dataStore.getUnavailableSGVCount() - 2) % 5)) * (POLL_PERIOD_MS / 10L); // adjust poll time in 1/10 steps to avoid potential poll clash (max adjustment at 5/10) } + } + MedtronicCnlAlarmManager.setAlarm(nextRequestedPollTime); + sendStatus("Next poll due at: " + dateFormatter.format(nextRequestedPollTime)); + } - // TODO - set status if offline or Nightscout not reachable - sendToXDrip(); - uploadToNightscout(); - MedtronicCnlAlarmReceiver.completeWakefulIntent(intent); + /** + * @return if device acquisition was successful + */ + private boolean openUsbDevice() { + if (!hasUsbHostFeature()) { + sendStatus("It appears that this device doesn't support USB OTG."); + Log.e(TAG, "Device does not support USB OTG"); + return false; + } + + UsbDevice cnlStick = UsbHidDriver.getUsbDevice(mUsbManager, USB_VID, USB_PID); + if (cnlStick == null) { + sendStatus("USB connection error. Is the Contour Next Link plugged in?"); + Log.w(TAG, "USB connection error. Is the CNL plugged in?"); + return false; + } + + if (!mUsbManager.hasPermission(UsbHidDriver.getUsbDevice(mUsbManager, USB_VID, USB_PID))) { + sendMessage(Constants.ACTION_NO_USB_PERMISSION); + return false; } + mHidDevice = UsbHidDriver.acquire(mUsbManager, cnlStick); + + try { + mHidDevice.open(); + } catch (Exception e) { + sendStatus("Unable to open USB device"); + Log.e(TAG, "Unable to open serial device", e); + return false; + } + + return true; } // reliable wake alarm manager wake up for all android versions @@ -298,6 +397,11 @@ public class MedtronicCnlIntentService extends IntentService { alarm.set(AlarmManager.RTC_WAKEUP, wakeTime, pendingIntent); } + private void uploadPollResults() { + sendToXDrip(); + uploadToNightscout(); + } + private void sendToXDrip() { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); if (prefs.getBoolean(getString(R.string.preference_enable_xdrip_plus), false)) { @@ -310,6 +414,7 @@ public class MedtronicCnlIntentService extends IntentService { } private void uploadToNightscout() { + // TODO - set status if offline or Nightscout not reachable Intent receiverIntent = new Intent(this, NightscoutUploadReceiver.class); final long timestamp = System.currentTimeMillis() + 1000L; final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, (int) timestamp, receiverIntent, PendingIntent.FLAG_ONE_SHOT); @@ -324,8 +429,8 @@ public class MedtronicCnlIntentService extends IntentService { public static final String ACTION_STATUS_MESSAGE = "info.nightscout.android.medtronic.service.STATUS_MESSAGE"; public static final String ACTION_NO_USB_PERMISSION = "info.nightscout.android.medtronic.service.NO_USB_PERMISSION"; 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..75847c83ce358679ce2ce14bcc0ae728675d4b13 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 @@ -45,13 +45,16 @@ public class PumpStatusEvent extends RealmObject { @Index private boolean uploaded = false; + public PumpStatusEvent() { + // The the eventDate to now. + this.eventDate = new Date(); + } + public Date getEventDate() { return eventDate; } - public void setEventDate(Date eventDate) { - this.eventDate = eventDate; - } + // No EventDate setter. The eventDate is set at the time that the PumpStatusEvent is created. public Date getPumpDate() { return pumpDate; @@ -78,17 +81,28 @@ public class PumpStatusEvent extends RealmObject { } public CGM_TREND getCgmTrend() { - return CGM_TREND.valueOf(cgmTrend); - } - - public void setCgmTrend(CGM_TREND cgmTrend) { - this.cgmTrend = cgmTrend.name(); + if (cgmTrend == null || !this.isCgmActive()) { + return CGM_TREND.NOT_SET; + } else { + return CGM_TREND.valueOf(cgmTrend); + } } public void setCgmTrend(String cgmTrend) { this.cgmTrend = cgmTrend; } + public String getCgmTrendString() { + return cgmTrend; + } + + public void setCgmTrend(CGM_TREND cgmTrend) { + if (cgmTrend != null) + this.cgmTrend = cgmTrend.name(); + else + this.cgmTrend = CGM_TREND.NOT_SET.name(); + } + public float getActiveInsulin() { return activeInsulin; } @@ -261,6 +275,38 @@ public class PumpStatusEvent extends RealmObject { this.pumpTimeOffset = pumpTimeOffset; } + @Override + public String toString() { + return "PumpStatusEvent{" + + "eventDate=" + eventDate + + ", pumpDate=" + pumpDate + + ", deviceName='" + deviceName + '\'' + + ", suspended=" + suspended + + ", bolusing=" + bolusing + + ", deliveringInsulin=" + deliveringInsulin + + ", tempBasalActive=" + tempBasalActive + + ", cgmActive=" + cgmActive + + ", activeBasalPattern=" + activeBasalPattern + + ", basalRate=" + basalRate + + ", tempBasalRate=" + tempBasalRate + + ", tempBasalPercentage=" + tempBasalPercentage + + ", tempBasalMinutesRemaining=" + tempBasalMinutesRemaining + + ", basalUnitsDeliveredToday=" + basalUnitsDeliveredToday + + ", batteryPercentage=" + batteryPercentage + + ", reservoirAmount=" + reservoirAmount + + ", minutesOfInsulinRemaining=" + minutesOfInsulinRemaining + + ", activeInsulin=" + activeInsulin + + ", sgv=" + sgv + + ", sgvDate=" + sgvDate + + ", lowSuspendActive=" + lowSuspendActive + + ", cgmTrend='" + cgmTrend + '\'' + + ", recentBolusWizard=" + recentBolusWizard + + ", bolusWizardBGL=" + bolusWizardBGL + + ", pumpTimeOffset=" + pumpTimeOffset + + ", uploaded=" + uploaded + + '}'; + } + public enum CGM_TREND { NONE, DOUBLE_UP, @@ -272,6 +318,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..b42a10614fb9f13c13dc06e9d87fb1459bfd166e 100644 --- a/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java +++ b/app/src/main/java/info/nightscout/android/settings/SettingsFragment.java @@ -1,5 +1,6 @@ package info.nightscout.android.settings; +import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; @@ -9,14 +10,25 @@ import android.preference.MultiSelectListPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceFragment; +import android.util.Log; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +import java.net.MalformedURLException; +import java.net.URL; import info.nightscout.android.R; public class SettingsFragment extends PreferenceFragment implements OnSharedPreferenceChangeListener { + private static final String TAG = SettingsFragment.class.getSimpleName(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final SettingsFragment that = this; /* set preferences */ addPreferencesFromResource(R.xml.preferences); @@ -25,11 +37,60 @@ 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")); + + Preference button = findPreference("scanButton"); + button.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + IntentIntegrator integrator = new IntentIntegrator(that); + integrator.initiateScan(); + + return true; + } + }); } @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 @@ -71,4 +132,55 @@ public class SettingsFragment extends PreferenceFragment implements OnSharedPref p.setSummary(editTextPref.getText()); } } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + + if (requestCode==IntentIntegrator.REQUEST_CODE) + { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data); + if (scanResult != null) + { + Log.d(TAG, "scanResult returns: " + scanResult.toString()); + + JsonParser json = new JsonParser(); + String resultContents = scanResult.getContents() == null ? "" : scanResult.getContents(); + JsonElement jsonElement = json.parse(resultContents); + if (jsonElement != null && jsonElement.isJsonObject()) { + jsonElement = (jsonElement.getAsJsonObject()).get("rest"); + if (jsonElement != null && jsonElement.isJsonObject()) { + jsonElement = (jsonElement.getAsJsonObject()).get("endpoint"); + if (jsonElement != null && jsonElement.isJsonArray() && jsonElement.getAsJsonArray().size() > 0) { + String endpoint = jsonElement.getAsJsonArray().get(0).getAsString(); + Log.d(TAG, "endpoint: " + endpoint); + + try { + URL uri = new URL(endpoint); + + StringBuilder url = new StringBuilder(uri.getProtocol()) + .append("://").append(uri.getHost()); + if (uri.getPort() > -1) + url.append(":").append(uri.getPort()); + + EditTextPreference editPref = (EditTextPreference) findPreference(getString(R.string.preference_nightscout_url)); + editPref.setText(url.toString()); + updatePrefSummary(editPref); + + editPref = (EditTextPreference) findPreference(getString(R.string.preference_api_secret)); + editPref.setText(uri.getUserInfo()); + updatePrefSummary(editPref); + } catch (MalformedURLException e) { + Log.w (TAG, e.getMessage()); + } + + } + } + } + } + else + { + Log.d(TAG, "scanResult is null."); + } + } + } } \ No newline at end of file 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..819f82e3923c37694a2ba17447dac2d82543ee0b --- /dev/null +++ b/app/src/main/java/info/nightscout/android/upload/nightscout/NightScoutUpload.java @@ -0,0 +1,180 @@ +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; + +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 = 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..038218934c13ccb0c2e99389977a5106ec8b536a 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,18 @@ 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 info.nightscout.android.utils.DataStore; 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 Realm mRealm; + + private Context mContext; + private NightScoutUpload mNightScoutUpload; public NightscoutUploadIntentService() { super(NightscoutUploadIntentService.class.getName()); @@ -64,12 +40,15 @@ public class NightscoutUploadIntentService extends IntentService { Log.i(TAG, "onCreate called"); mContext = this.getBaseContext(); + + mNightScoutUpload = new NightScoutUpload(); + } @Override protected void onHandleIntent(Intent intent) { Log.d(TAG, "onHandleIntent called"); - mRealm = Realm.getDefaultInstance(); + Realm mRealm = Realm.getDefaultInstance(); RealmResults<PumpStatusEvent> records = mRealm .where(PumpStatusEvent.class) @@ -85,7 +64,17 @@ 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"); + Boolean uploadSuccess = mNightScoutUpload.doRESTUpload(urlSetting, + secretSetting, DataStore.getInstance().getUploaderBatteryLevel(), 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 +83,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 +99,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..1f40ab015abd146de2c596efb414ad798df58594 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,37 @@ 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? + } + } + + // TODO currentnly unused, see info.nightscout.android.xdrip_plus.XDripPlusUploadIntentService.addSgvEntry() + // TODO also, proper method name @Override public JsonElement serialize(PumpStatusEvent src, Type typeOfSrc, JsonSerializationContext context) { final JsonObject jsonObject = new JsonObject(); @@ -49,8 +80,8 @@ public class EntriesSerializer implements JsonSerializer<PumpStatusEvent> { jsonObject.addProperty("direction", getDirectionString(src.getCgmTrend())); jsonObject.addProperty("device", src.getDeviceName()); jsonObject.addProperty("type", "sgv"); - jsonObject.addProperty("date", src.getEventDate().getTime()); - jsonObject.addProperty("dateString", String.valueOf(src.getEventDate())); + jsonObject.addProperty("date", src.getSgvDate().getTime()); + jsonObject.addProperty("dateString", String.valueOf(src.getSgvDate())); return jsonObject; } diff --git a/app/src/main/java/info/nightscout/android/utils/ConfigurationStore.java b/app/src/main/java/info/nightscout/android/utils/ConfigurationStore.java new file mode 100644 index 0000000000000000000000000000000000000000..a45c63f40b50e8771371de624cb05ffc7e8dabd5 --- /dev/null +++ b/app/src/main/java/info/nightscout/android/utils/ConfigurationStore.java @@ -0,0 +1,66 @@ +package info.nightscout.android.utils; + + +import info.nightscout.android.medtronic.service.MedtronicCnlIntentService; + +/** + * Created by volker on 30.03.2017. + */ + +public class ConfigurationStore { + private static ConfigurationStore instance; + + private boolean reducePollOnPumpAway = false; + private long pollInterval = MedtronicCnlIntentService.POLL_PERIOD_MS; + private long lowBatteryPollInterval = MedtronicCnlIntentService.LOW_BATTERY_POLL_PERIOD_MS; + private boolean mmolxl; + private boolean mmolxlDecimals; + + public static ConfigurationStore getInstance() { + if (ConfigurationStore.instance == null) { + instance = new ConfigurationStore(); + } + + return instance; + } + + public boolean isReducePollOnPumpAway() { + return reducePollOnPumpAway; + } + + public void setReducePollOnPumpAway(boolean reducePollOnPumpAway) { + this.reducePollOnPumpAway = reducePollOnPumpAway; + } + + public long getPollInterval() { + return pollInterval; + } + + public void setPollInterval(long pollInterval) { + this.pollInterval = pollInterval; + } + + public long getLowBatteryPollInterval() { + return lowBatteryPollInterval; + } + + public void setLowBatteryPollInterval(long lowBatteryPollInterval) { + this.lowBatteryPollInterval = lowBatteryPollInterval; + } + + public boolean isMmolxl() { + return mmolxl; + } + + public void setMmolxl(boolean mmolxl) { + this.mmolxl = mmolxl; + } + + public boolean isMmolxlDecimals() { + return mmolxlDecimals; + } + + public void setMmolxlDecimals(boolean mmolxlDecimals) { + this.mmolxlDecimals = mmolxlDecimals; + } +} diff --git a/app/src/main/java/info/nightscout/android/utils/DataStore.java b/app/src/main/java/info/nightscout/android/utils/DataStore.java new file mode 100644 index 0000000000000000000000000000000000000000..39fe120d971acda594d91203e040fd2125c2a3de --- /dev/null +++ b/app/src/main/java/info/nightscout/android/utils/DataStore.java @@ -0,0 +1,79 @@ +package info.nightscout.android.utils; + + +import java.util.Date; + +import info.nightscout.android.model.medtronicNg.PumpStatusEvent; +import io.realm.Realm; + +/** + * Created by volker on 30.03.2017. + */ + +public class DataStore { + private static DataStore instance; + + private PumpStatusEvent lastPumpStatus; + private int uploaderBatteryLevel = 0; + private int unavailableSGVCount = 0; + private long activePumpMac = 0; + + private DataStore() {} + + public static DataStore getInstance() { + if (DataStore.instance == null) { + instance = new DataStore(); + + // set some initial dummy values + PumpStatusEvent dummyStatus = new PumpStatusEvent(); + dummyStatus.setSgvDate(new Date()); + + // bypass setter to avoid dealing with a real Realm object + instance.lastPumpStatus = dummyStatus; + } + + return instance; + } + + public PumpStatusEvent getLastPumpStatus() { + return lastPumpStatus; + } + + public void setLastPumpStatus(PumpStatusEvent lastPumpStatus) { + Realm realm = Realm.getDefaultInstance(); + + this.lastPumpStatus = realm.copyFromRealm(lastPumpStatus); + if (!realm.isClosed()) realm.close(); + } + + public int getUploaderBatteryLevel() { + return uploaderBatteryLevel; + } + + public void setUploaderBatteryLevel(int uploaderBatteryLevel) { + this.uploaderBatteryLevel = uploaderBatteryLevel; + } + + public int getUnavailableSGVCount() { + return unavailableSGVCount; + } + + public int incUnavailableSGVCount() { + return unavailableSGVCount++; + } + + public void clearUnavailableSGVCount() { + this.unavailableSGVCount = 0; + } + public void setUnavailableSGVCount(int unavailableSGVCount) { + this.unavailableSGVCount = unavailableSGVCount; + } + + public long getActivePumpMac() { + return activePumpMac; + } + + public void setActivePumpMac(long activePumpMac) { + this.activePumpMac = activePumpMac; + } +} 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..002ab6b2b69f925b9ba39a44288991bc6416fc10 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,14 @@ 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(" ?") + .append(HEX_DIGITS[i]); + } result.append("\n0x"); + result.append(toHexString(offset)); for (int i = offset; i < offset + length; i++) { @@ -63,19 +70,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("."); } } @@ -102,6 +107,7 @@ public class HexDump { return new String(buf); } + public static String toHexString(int i) { return toHexString(toByteArray(i)); } @@ -137,8 +143,8 @@ public class HexDump { public static byte[] hexStringToByteArray(String hexString) { int length = hexString.length(); byte[] buffer = new byte[length / 2]; - if (length% 2 == 1) - length--; + if (length % 2 == 1) + length--; for (int i = 0; i < length; i += 2) { buffer[i / 2] = (byte) ((toByte(hexString.charAt(i)) << 4) | toByte(hexString .charAt(i + 1))); @@ -146,36 +152,39 @@ public class HexDump { return buffer; } - - public static int unsignedByte(byte b){ - return (b & 0xFF); + + public static int unsignedByte(byte b) { + return (b & 0xFF); } - - public static byte bUnsignedByte(byte b){ - return (byte)(b & 0xFF); + + public static byte bUnsignedByte(byte b) { + return (byte) (b & 0xFF); } + + @SuppressWarnings("ResultOfMethodCallIgnored") public static boolean isHexaNumber(String cadena) { try { - Long.parseLong(cadena,16); + Long.parseLong(cadena, 16); return true; } catch (NumberFormatException nfe) { return false; } } - - public static int byteArrayToInt (byte[] arr){ - int length = arr.length; - int mult = 1; - int res = 0; - if (length > 0 && length <5){ - for (int i = length-1; i >= 0; i--){ - res += unsignedByte(arr[i])*mult; - mult *=256; - } - } - return res; + + public static int byteArrayToInt(byte[] arr) { + int length = arr.length; + int mult = 1; + int res = 0; + if (length > 0 && length < 5) { + for (int i = length - 1; i >= 0; i--) { + res += unsignedByte(arr[i]) * mult; + mult *= 256; + } + } + return res; } - public static short byteArrayToShort (byte[] arr){ - return (short) (unsignedByte(arr[0])*256 + unsignedByte(arr[1])); + + public static short byteArrayToShort(byte[] arr) { + return (short) (unsignedByte(arr[0]) * 256 + unsignedByte(arr[1])); } } 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..e06a88b8cf047dde4d5a812b58198e69bd508573 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 @@ -16,9 +16,9 @@ import java.text.SimpleDateFormat; import java.util.List; import java.util.Locale; -import info.nightscout.android.medtronic.MainActivity; import info.nightscout.android.model.medtronicNg.PumpStatusEvent; import info.nightscout.android.upload.nightscout.serializer.EntriesSerializer; +import info.nightscout.android.utils.DataStore; import io.realm.Realm; import io.realm.RealmResults; import io.realm.Sort; @@ -33,7 +33,6 @@ public class XDripPlusUploadIntentService extends IntentService { private static final String TAG = XDripPlusUploadIntentService.class.getSimpleName(); private static final SimpleDateFormat ISO8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); Context mContext; - private Realm mRealm; public XDripPlusUploadIntentService() { super(XDripPlusUploadIntentService.class.getName()); @@ -58,7 +57,7 @@ public class XDripPlusUploadIntentService extends IntentService { @Override protected void onHandleIntent(Intent intent) { Log.d(TAG, "onHandleIntent called"); - mRealm = Realm.getDefaultInstance(); + Realm mRealm = Realm.getDefaultInstance(); RealmResults<PumpStatusEvent> all_records = mRealm .where(PumpStatusEvent.class) @@ -70,6 +69,7 @@ public class XDripPlusUploadIntentService extends IntentService { List<PumpStatusEvent> records = all_records.subList(0, 1); doXDripUpload(records); } + mRealm.close(); XDripPlusUploadReceiver.completeWakefulIntent(intent); } @@ -85,10 +85,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,14 +106,16 @@ 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"); + } } private void addDeviceStatus(JSONArray devicestatusArray, PumpStatusEvent record) throws Exception { JSONObject json = new JSONObject(); - json.put("uploaderBattery", MainActivity.batLevel); + json.put("uploaderBattery", DataStore.getInstance().getUploaderBatteryLevel()); json.put("device", record.getDeviceName()); json.put("created_at", ISO8601_DATE_FORMAT.format(record.getPumpDate())); @@ -141,8 +145,8 @@ public class XDripPlusUploadIntentService extends IntentService { 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()); + json.put("date", pumpRecord.getSgvDate().getTime()); + json.put("dateString", pumpRecord.getSgvDate()); entriesArray.put(json); } 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..f528c75f14698284d48e3cb0c5bc811ee6817ad6 --- /dev/null +++ b/app/src/main/java/info/nightscout/api/DeviceEndpoints.java @@ -0,0 +1,96 @@ +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.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..a2c4a1a322a6c897c2e49b40b52f1ab52ee536a5 --- /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/drawable/drawer_header.jpg b/app/src/main/res/drawable/drawer_header.jpg index 251abd5c8a8ee193ef484393a966c891c8f5478a..2f33ff7e47ebc0f2ea9af68e5a0d58b183a38bed 100644 Binary files a/app/src/main/res/drawable/drawer_header.jpg and b/app/src/main/res/drawable/drawer_header.jpg 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..9810c1ad7339738df8fb7cde34bbd38dbee7c78a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -52,8 +52,9 @@ android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_weight="1" - android:singleLine="true" + android:maxLines="1" android:text="-" + android:textAlignment="center" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="70sp" /> @@ -70,7 +71,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center|top" - android:singleLine="true" + android:maxLines="1" android:text="-" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="40sp" /> @@ -80,9 +81,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:singleLine="true" android:text="mmol/L" - android:textAppearance="?android:attr/textAppearanceSmall" /> + android:textAppearance="?android:attr/textAppearanceSmall" + android:maxLines="1" /> </LinearLayout> @@ -119,11 +120,10 @@ </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" @@ -140,7 +140,8 @@ 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..620622de5bc12f18f4abd312f6a0c5659ac7ba6f 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"> @@ -25,40 +26,91 @@ </android.support.design.widget.AppBarLayout> <ScrollView + android:id="@+id/status_scroll_view" android:layout_width="fill_parent" android:layout_height="fill_parent" - android:id="@+id/status_scroll_view" - android:fillViewport="true" > + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:fillViewport="true"> <LinearLayout - android:orientation="vertical" + android:id="@+id/x" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:layout_margin="5dp" + android:orientation="vertical"> <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Pump Status" android:id="@+id/status_pump_text_view" - android:singleLine="true" /> + style="?android:attr/listSeparatorTextViewStyle" + android:layout_height="wrap_content" + android:maxLines="1" + android:text="Pump Status" /> + + <GridLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <android.support.v7.widget.CardView + android:id="@+id/card_view" + android:layout_width="100dp" + android:layout_height="100dp" + android:layout_gravity="center" + app:cardCornerRadius="4dp"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="5dp"> + + <ImageView + android:id="@+id/imageView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="center" + app:srcCompat="@drawable/battery_0" /> + + <TextView + android:id="@+id/textView2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/textView" + android:text="Units Remaining" + android:textAppearance="@style/TextAppearance.AppCompat.Caption" /> + + <TextView + android:id="@+id/textView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/imageView" + android:layout_centerHorizontal="true" + android:layout_centerInParent="true" + android:text="150.250" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.AppCompat.Headline" /> + </RelativeLayout> + + </android.support.v7.widget.CardView> + + </GridLayout> <TextView - android:layout_width="wrap_content" + android:id="@+id/status_uploader_text_view" + style="?android:attr/listSeparatorTextViewStyle" android:layout_height="wrap_content" - android:text="Uploader Status" - android:id="@+id/status_uploader_text_view" /> + android:text="Uploader Status" /> <TextView - android:layout_width="wrap_content" + android:id="@+id/status_cgm_text_view" + style="?android:attr/listSeparatorTextViewStyle" android:layout_height="wrap_content" - android:text="CGM Status" - android:id="@+id/status_cgm_text_view" /> + android:text="CGM Status" /> <TextView - android:layout_width="wrap_content" + android:id="@+id/status_nightscout_text_view" + style="?android:attr/listSeparatorTextViewStyle" android:layout_height="wrap_content" - android:text="Nightscout Status" - android:id="@+id/status_nightscout_text_view" /> + android:text="Nightscout Status" /> </LinearLayout> </ScrollView> 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..2fb909e0027846c2b164a33e14ba004a6785bf10 --- /dev/null +++ b/app/src/main/res/layout/cnl_item.xml @@ -0,0 +1,25 @@ +<?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:paddingRight="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..5eb735dd3b5aa22f786779f76b01059c5917e7a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - - <string name="hello">---</string> - <string name="app_name">NS 640g Uploader</string> + <string name="app_name">600 Series Uploader</string> <string name="eula_title">Disclaimer</string> <string name="eula_accept">Accept</string> <string name="eula_refuse">Refuse</string> @@ -16,29 +14,11 @@ <item>Info</item> <item>Debug</item> </string-array> - <string name="title_activity_login">CareLink login</string> <!-- Strings related to login --> - <string name="prompt_username">CareLink Username</string> - <string name="prompt_password">Password</string> - <string name="action_sign_in">Retrieve keys for USB</string> - <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_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> <string name="preferences_enable_crashlytics">prefs_enable_crashlytics</string> <string name="preferences_enable_answers">prefs_enable_answers</string> <string name="preferences_enable_remote_logcat">prefs_enable_remote_logcat</string> - <string name="menu_name_preferences">Preferences</string> - <string name="button_text_stop_uploading_data">Stop Uploading CGM Data</string> - <string name="button_text_clear_log">Clear Log</string> - <string name="button_text_get_now">Get Now</string> - <string name="button_text_start_uploading_data">Start Uploading CGM Data</string> <string name="preference_eula_accepted">IUNDERSTAND</string> <string name="preference_enable_rest_upload">EnableRESTUpload</string> <string name="preference_enable_xdrip_plus">EnablexDripPlusUpload</string> @@ -46,8 +26,16 @@ <string name="text_unit_mmolxl">mmol/L</string> <string name="text_unit_mgxdl">mg/dL</string> - <string name="title_activity_status">Uploader Status</string> - <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..ff89a6f2bb0e9027c0f4fc857a79595b7a6fe4d5 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 @@ -39,6 +70,10 @@ android:dialogTitle="Enter your Nightscout API secret" android:key="@string/preference_api_secret" android:title="API Secret"/> + <Preference android:title="scan NS-Autoconfig QR-Code" + android:key="scanButton" + android:dependency="@string/preference_enable_rest_upload" + android:summary="Click here to scan QR-Code from http://nightscout.github.io/pages/configure/ using ZXing barcode scanner."/> <CheckBoxPreference android:key="@string/preference_enable_xdrip_plus" android:summary="Enable local broadcast of data to xDrip+" diff --git a/build.gradle b/build.gradle index 9da42b0b994170c75de5ee547bfabc0007cf8dd7..958eeb218133aa2d20df86abbece04f6df2ed49b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,7 @@ buildscript { jcenter() } dependencies { - 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 'com.android.tools.build:gradle:2.3.3' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3d5435e07e9964062e47fc0426eaa27552a17041..8e8e1988d58c6650b6e10423666fdeef466bac95 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Nov 12 11:44:13 AEDT 2016 +#Tue Mar 28 09:13:19 AEDT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip