From 238b3c504f6e4a713d88df9ada937be5fd189c4e Mon Sep 17 00:00:00 2001 From: Graham Jones Date: Sat, 22 Jun 2019 11:41:58 +0100 Subject: [PATCH] V3.1.9 - quite a few changes - see Changelog.md --- CHANGELOG.md | 5 + app/src/main/AndroidManifest.xml | 4 +- .../org/openseizuredetector/MainActivity.java | 17 +- .../uk/org/openseizuredetector/OsdUtil.java | 1 + .../org/openseizuredetector/SdAnalyser.java | 195 ++++++++++++++++++ .../uk/org/openseizuredetector/SdData.java | 17 +- .../openseizuredetector/SdDataSourceAw.java | 8 +- .../SdDataSourceGarmin.java | 186 ++++++++++------- .../SdDataSourceNetwork.java | 10 +- .../SdDataSourcePebble.java | 13 +- .../uk/org/openseizuredetector/SdServer.java | 2 +- .../SdServiceConnection.java | 4 +- .../openseizuredetector/StartupActivity.java | 4 +- .../EventLogManagerTest.java | 46 ----- .../openseizuredetector/SdAnalyserTest.java | 90 ++++++++ build.gradle | 2 +- 16 files changed, 435 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/uk/org/openseizuredetector/SdAnalyser.java delete mode 100644 app/src/test/java/uk/org/openseizuredetector/EventLogManagerTest.java create mode 100644 app/src/test/java/uk/org/openseizuredetector/SdAnalyserTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9e19b..561fe38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ V3.2.0 - (NEXT VERSION!) - Added neural network based data analysis. + V3.1.9 - 14jun2019 + - Now requests READ_PHONE_STATE along with SMS permissions (required for some phones to send SMS messages) + - Fixed issue with Garmin Seizure Detector not producing warnings. + - Added faut pips for missing heart rate data if heart rate alarm active + V3.1.8 - 06jun2019 - Added READ_PHONE_STATE permission which seems to be needed for some phones to send SMS (but not many). diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1112943..9588f68 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ diff --git a/app/src/main/java/uk/org/openseizuredetector/MainActivity.java b/app/src/main/java/uk/org/openseizuredetector/MainActivity.java index 63460a8..c74edd2 100644 --- a/app/src/main/java/uk/org/openseizuredetector/MainActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/MainActivity.java @@ -63,8 +63,6 @@ import com.github.mikephil.charting.data.BarEntry; import com.github.mikephil.charting.utils.ValueFormatter; import com.rohitss.uceh.UCEHandler; -import static java.util.Objects.isNull; - public class MainActivity extends AppCompatActivity { static final String TAG = "MainActivity"; private int okColour = Color.BLUE; @@ -435,15 +433,18 @@ public class MainActivity extends AppCompatActivity { tv = (TextView) findViewById(R.id.pebbleTv); if (mConnection.mSdServer.mSdData.mHRAlarmActive) { tv.setText("HR = " + mConnection.mSdServer.mSdData.mHR); - if (!mConnection.mSdServer.mSdData.mHRAlarmStanding) { - tv.setBackgroundColor(okColour); - tv.setTextColor(okTextColour); - } else { + if (mConnection.mSdServer.mSdData.mHRAlarmStanding) { tv.setBackgroundColor(alarmColour); tv.setTextColor(alarmTextColour); + } else if (mConnection.mSdServer.mSdData.mHRFaultStanding) { + tv.setBackgroundColor(warnColour); + tv.setTextColor(warnTextColour); + } else { + tv.setBackgroundColor(okColour); + tv.setTextColor(okTextColour); } } else { - if (mConnection.mSdServer.mSdData.pebbleConnected) { + if (mConnection.mSdServer.mSdData.watchConnected) { tv.setText("HR Alarm OFF"); tv.setBackgroundColor(okColour); tv.setTextColor(okTextColour); @@ -455,7 +456,7 @@ public class MainActivity extends AppCompatActivity { } } tv = (TextView) findViewById(R.id.appTv); - if (mConnection.mSdServer.mSdData.pebbleAppRunning) { + if (mConnection.mSdServer.mSdData.watchAppRunning) { tv.setText("Watch App OK"); tv.setBackgroundColor(okColour); tv.setTextColor(okTextColour); diff --git a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java index 84a7ba1..26c46bd 100644 --- a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java +++ b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java @@ -96,6 +96,7 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac private final String[] SMS_PERMISSIONS = { Manifest.permission.SEND_SMS, Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.READ_PHONE_STATE, }; diff --git a/app/src/main/java/uk/org/openseizuredetector/SdAnalyser.java b/app/src/main/java/uk/org/openseizuredetector/SdAnalyser.java new file mode 100644 index 0000000..2127d9b --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/SdAnalyser.java @@ -0,0 +1,195 @@ +/* + Android_Pebble_SD - a simple accelerometer based seizure detector that runs on a + Pebble smart watch (http://getpebble.com). + + See http://openseizuredetector.org for more information. + + Copyright Graham Jones, 2015, 2016, 2017, 2018, 2019. + + This file is part of android_pebble_sd. + + Android_pebble_sd is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Android_pebble_sd is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with android_pebble_sd. If not, see . + +*/ +package uk.org.openseizuredetector; + +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.jtransforms.fft.DoubleFFT_1D; + +/** + * SdAnalyser implements the seizure detection algorithm on an Android Phone - it is used by SdDatasourceGarmin + */ +public class SdAnalyser { + private String TAG = "SdAnalyser"; + private double mSampleFreq; + private double mAlarmFreqMin; + private double mAlarmFreqMax; + private double mSamplePeriod; + private double mWarnTime; + private double mAlarmTime; + private double mAlarmThresh; + private double mAlarmRatioThresh; + private double mFreqRes; + private int mAlarmCount; + private double mFreqCutoff; + private int mNSamp; + + double roiPower; + double specPower; + double roiRatio; + + SdAnalyser(double sampleFreq, + double alarmFreqMin, + double alarmFreqMax, + double samplePeriod, + double warnTime, + double alarmThresh, + double alarmRatioThresh) { + mSampleFreq = sampleFreq; + mAlarmFreqMin = alarmFreqMin; + mAlarmFreqMax = alarmFreqMax; + mSamplePeriod = samplePeriod; + mWarnTime = warnTime; + mAlarmThresh = alarmThresh; + mAlarmRatioThresh = alarmRatioThresh; + + mFreqRes = 1.0 / mSamplePeriod; + mFreqCutoff = mSampleFreq / 2.0; + mNSamp = (int)(mSamplePeriod * mSampleFreq); + } + + int freq2fftBin(double freq) { + int n = (int)(freq/mFreqRes); + return(n); + } + + /** + * doAnalysis() - analyse the data if the accelerometer data array mAccData + * and populate the output data structure mSdData + */ + void calculateSpectralPowers(double[] rawData) { + // Set the frequency bounds for the analysis in fft output bin numbers. + int nMin = freq2fftBin(mAlarmFreqMin); + int nMax = freq2fftBin(mAlarmFreqMax); + int nFreqCutoff = freq2fftBin(mFreqCutoff); + + DoubleFFT_1D fftDo = new DoubleFFT_1D(mNSamp); + double[] fft = new double[mNSamp * 2]; + System.arraycopy(rawData, 0, fft, 0, mNSamp); + fftDo.realForward(fft); + + // Calculate the whole spectrum power (well a value equivalent to it that avoids square root calculations + // and zero any readings that are above the frequency cutoff. + double specPower = 0; + for (int i = 1; i < mNSamp / 2; i++) { + if (i <= nFreqCutoff) { + specPower = specPower + getMagnitude(fft, i); + } else { + fft[2 * i] = 0.; + fft[2 * i + 1] = 0.; + } + } + specPower = specPower / (mNSamp / 2); + + // Calculate the Region of Interest power and power ratio. + double roiPower = 0; + for (int i = nMin; i < nMax; i++) { + roiPower = roiPower + getMagnitude(fft, i); + } + roiPower = roiPower / (nMax - nMin); + double roiRatio = 10 * roiPower / specPower; + + } + + /** + * Calculate the magnitude of entry i in the fft array fft + * + * @param fft + * @param i + * @return magnitude ( Re*Re + Im*Im ) + */ + double getMagnitude(double[] fft, int i) { + double mag; + mag = (fft[2 * i] * fft[2 * i] + fft[2 * i + 1] * fft[2 * i + 1]); + return mag; + } + + // Force the data stored in this datasource to update in line with the JSON string encoded data provided. + // Used by webServer to update the GarminDatasource. + // Returns a message string that is passed back to the watch. + public String updateFromJSON(String jsonStr) { + String retVal = "undefined"; + Log.v(TAG,"updateFromJSON - "+jsonStr); + + try { + JSONObject mainObject = new JSONObject(jsonStr); + //JSONObject dataObject = mainObject.getJSONObject("dataObj"); + JSONObject dataObject = mainObject; + String dataTypeStr = dataObject.getString("dataType"); + Log.v(TAG,"updateFromJSON - dataType="+dataTypeStr); + if (dataTypeStr.equals("raw")) { + Log.v(TAG,"updateFromJSON - processing raw data"); + try { + mSdData.mHR = dataObject.getDouble("HR"); + } catch (JSONException e) { + // if we get 'null' HR (For example if the heart rate is not working) + mSdData.mHR = -1; + } + try { + mMute = dataObject.getInt("Mute"); + } catch (JSONException e) { + // if we get 'null' HR (For example if the heart rate is not working) + mMute = 0; + } + JSONArray accelVals = dataObject.getJSONArray("data"); + Log.v(TAG, "Received " + accelVals.length() + " acceleration values"); + int i; + for (i = 0; i < accelVals.length(); i++) { + mSdData.rawData[i] = accelVals.getInt(i); + } + mSdData.mNsamp = accelVals.length(); + //mNSamp = accelVals.length(); + mWatchAppRunningCheck = true; + doAnalysis(); + if (mSdData.haveSettings == false) { + retVal = "sendSettings"; + } else { + retVal = "OK"; + } + } else if (dataTypeStr.equals("settings")){ + Log.v(TAG,"updateFromJSON - processing settings"); + mSamplePeriod = (short)dataObject.getInt("analysisPeriod"); + mSampleFreq = (short)dataObject.getInt("sampleFreq"); + mSdData.batteryPc = (short)dataObject.getInt("battery"); + Log.v(TAG,"updateFromJSON - mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq); + mSdData.haveSettings = true; + mSdData.mSampleFreq = mSampleFreq; + mWatchAppRunningCheck = true; + retVal = "OK"; + } else { + Log.e(TAG,"updateFromJSON - unrecognised dataType "+dataTypeStr); + retVal = "ERROR"; + } + } catch (Exception e) { + Log.e(TAG,"updateFromJSON - Error Parsing JSON String - "+e.toString()); + e.printStackTrace(); + retVal = "ERROR"; + } + return(retVal); + } +} diff --git a/app/src/main/java/uk/org/openseizuredetector/SdData.java b/app/src/main/java/uk/org/openseizuredetector/SdData.java index 9794358..7b4974b 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdData.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdData.java @@ -29,10 +29,6 @@ import android.os.Parcel; import android.text.format.Time; import android.util.Log; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Locale; - import org.json.JSONObject; import org.json.JSONArray; @@ -82,11 +78,12 @@ public class SdData implements Parcelable { public long roiPower; public String alarmPhrase; public int simpleSpec[]; - public boolean pebbleConnected = false; - public boolean pebbleAppRunning = false; + public boolean watchConnected = false; + public boolean watchAppRunning = false; public boolean serverOK = false; public boolean mHRAlarmStanding = false; + public boolean mHRFaultStanding = false; public double mHR = 0; public SdData() { @@ -116,8 +113,8 @@ public class SdData implements Parcelable { specPower = jo.optInt("specPower"); roiPower = jo.optInt("roiPower"); batteryPc = jo.optInt("batteryPc"); - pebbleConnected = jo.optBoolean("pebbleConnected"); - pebbleAppRunning = jo.optBoolean("pebbleAppRunning"); + watchConnected = jo.optBoolean("watchConnected"); + watchAppRunning = jo.optBoolean("watchAppRunning"); alarmState = jo.optInt("alarmState"); alarmPhrase = jo.optString("alarmPhrase"); alarmThresh = jo.optInt("alarmThresh"); @@ -159,8 +156,8 @@ public class SdData implements Parcelable { jsonObj.put("specPower", specPower); jsonObj.put("roiPower", roiPower); jsonObj.put("batteryPc", batteryPc); - jsonObj.put("pebbleConnected", pebbleConnected); - jsonObj.put("pebbleAppRunning", pebbleAppRunning); + jsonObj.put("watchConnected", watchConnected); + jsonObj.put("watchAppRunning", watchAppRunning); jsonObj.put("haveSettings", haveSettings); jsonObj.put("alarmState", alarmState); jsonObj.put("alarmPhrase", alarmPhrase); diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java index e96b578..60e1dde 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java @@ -683,8 +683,8 @@ public class SdDataSourceAw extends SdDataSource { tdiff = (tnow.toMillis(false) - mStatusTime.toMillis(false)); Log.v(TAG, "getStatus() - mWatchAppRunningCheck=" + mWatchAppRunningCheck + " tdiff=" + tdiff); // Check we are actually connected to the pebble. - mSdData.pebbleConnected = PebbleKit.isWatchConnected(mContext); - if (!mSdData.pebbleConnected) mWatchAppRunningCheck = false; + mSdData.watchConnected = PebbleKit.isWatchConnected(mContext); + if (!mSdData.watchConnected) mWatchAppRunningCheck = false; // And is the pebble_sd app running? // set mWatchAppRunningCheck has been false for more than 10 seconds // the app is not talking to us @@ -692,7 +692,7 @@ public class SdDataSourceAw extends SdDataSource { if (!mWatchAppRunningCheck && (tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) { Log.v(TAG, "getStatus() - tdiff = " + tdiff); - mSdData.pebbleAppRunning = false; + mSdData.watchAppRunning = false; //Log.v(TAG, "getStatus() - Pebble App Not Running - Attempting to Re-Start"); //mUtil.writeToSysLogFile("SdDataSourceAw.getStatus() - Pebble App not Running - Attempting to Re-Start"); //startWatchApp(); @@ -709,7 +709,7 @@ public class SdDataSourceAw extends SdDataSource { Log.v(TAG, "getStatus() - Waiting for mFaultTimerPeriod before issuing audible warning..."); } } else { - mSdData.pebbleAppRunning = true; + mSdData.watchAppRunning = true; } // if we have confirmation that the app is running, reset the diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java index ac3c02a..41c3330 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java @@ -28,32 +28,22 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.os.BatteryManager; import android.os.Handler; import android.preference.PreferenceManager; import android.text.format.Time; import android.util.Log; import android.widget.Toast; -import com.getpebble.android.kit.PebbleKit; -import com.getpebble.android.kit.util.PebbleDictionary; - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.jtransforms.fft.DoubleFFT_1D; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.IntBuffer; import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; -import java.util.UUID; import static java.lang.Long.parseLong; -import static java.lang.Math.sqrt; /** @@ -66,7 +56,7 @@ public class SdDataSourceGarmin extends SdDataSource { private Handler mHandler = new Handler(); private Timer mStatusTimer; private Timer mSettingsTimer; - private Timer mAlarmCheckTimer; + private Timer mFaultCheckTimer; private Time mDataStatusTime; private boolean mWatchAppRunningCheck = false; private int mAppRestartTimeout = 10; // Timeout before re-starting watch app (sec) if we have not received @@ -145,14 +135,14 @@ public class SdDataSourceGarmin extends SdDataSource { Log.v(TAG, "start(): status timer already running."); mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - status timer already running??"); } - if (mAlarmCheckTimer == null) { + if (mFaultCheckTimer == null) { Log.v(TAG, "start(): starting alarm check timer"); mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - starting alarm check timer"); - mAlarmCheckTimer = new Timer(); - mAlarmCheckTimer.schedule(new TimerTask() { + mFaultCheckTimer = new Timer(); + mFaultCheckTimer.schedule(new TimerTask() { @Override public void run() { - alarmCheck(); + faultCheck(); } }, 0, 1000); } else { @@ -206,12 +196,12 @@ public class SdDataSourceGarmin extends SdDataSource { mSettingsTimer = null; } // Stop the alarm check timer - if (mAlarmCheckTimer != null) { + if (mFaultCheckTimer != null) { Log.v(TAG, "stop(): cancelling alarm check timer"); mUtil.writeToSysLogFile("SdDataSourceGarmin.stop() - cancelling alarm check timer"); - mAlarmCheckTimer.cancel(); - mAlarmCheckTimer.purge(); - mAlarmCheckTimer = null; + mFaultCheckTimer.cancel(); + mFaultCheckTimer.purge(); + mFaultCheckTimer = null; } } catch (Exception e) { @@ -467,7 +457,10 @@ public class SdDataSourceGarmin extends SdDataSource { fft[2*i+1] = 0.; } } + //Log.v(TAG,"specPower = "+specPower); + //specPower = specPower/(mSdData.mNsamp/2); specPower = specPower/mSdData.mNsamp/2; + //Log.v(TAG,"specPower = "+specPower); // Calculate the Region of Interest power and power ratio. double roiPower = 0; @@ -489,8 +482,6 @@ public class SdDataSourceGarmin extends SdDataSource { simpleSpec[ifreq] = simpleSpec[ifreq] / (binMax-binMin); } - checkFall(); - // Populate the mSdData structure to communicate with the main SdServer service. mDataStatusTime.setToNow(); mSdData.specPower = (long)specPower / ACCEL_SCALE_FACTOR; @@ -512,15 +503,102 @@ public class SdDataSourceGarmin extends SdDataSource { // Because we have received data, set flag to show watch app running. mWatchAppRunningCheck = true; + + // Check this data to see if it represents an alarm state. + alarmCheck(); + hrCheck(); + fallCheck(); + muteCheck(); + mSdDataReceiver.onSdDataReceived(mSdData); // and tell SdServer we have received data. } + /**************************************************************** + * checkAlarm() - checks the current accelerometer data and uses + * historical data to determine if we are in a fault, warning or ok + * state. + * Sets mSdData.alarmState and mSdData.hrAlarmStanding + */ + private void alarmCheck() { + boolean inAlarm; + Log.v(TAG, "alarmCheck()"); + // Is the current set of data representing an alarm state? + if ((mSdData.roiPower > mAlarmThresh) && (10 * (mSdData.roiPower / mSdData.specPower) > mAlarmRatioThresh)) { + inAlarm = true; + } else { + inAlarm = false; + } + + // set the alarmState to Alarm, Warning or OK, depending on the current state and previous ones. + if (inAlarm) { + mAlarmCount += mSamplePeriod; + if (mAlarmCount > mAlarmTime) { + // full alarm + mSdData.alarmState = 2; + } else if (mAlarmCount > mWarnTime) { + // warning + mSdData.alarmState = 1; + } + } else { + // If we are not in an ALARM state, revert back to WARNING, otherwise + // revert back to OK. + if (mSdData.alarmState == 2) { + // revert to warning + mSdData.alarmState = 1; + mAlarmCount = mWarnTime + 1; // pretend we have only just entered warning state. + } else { + // revert to OK + mSdData.alarmState = 0; + mAlarmCount = 0; + } + } + + Log.v(TAG, "alarmCheck(): inAlarm=" + inAlarm + ", alarmState = " + mSdData.alarmState + " alarmCount=" + mAlarmCount + " mAlarmTime=" + mAlarmTime); + + } + + public void muteCheck() { + if (mMute != 0) { + Log.v(TAG, "Mute Active - setting alarms to mute"); + mSdData.alarmState = 6; + mSdData.alarmPhrase = "MUTE"; + mSdData.mHRAlarmStanding = false; + } + + } + + /** + * hrCheck - check the Heart rate data in mSdData to see if it represents an alarm condition. + * Sets mSdData.mHRAlarmStanding + */ + public void hrCheck() { + Log.v(TAG, "hrCheck()"); + /* Check Heart Rate against alarm settings */ + if (mSdData.mHRAlarmActive) { + if (mSdData.mHR < 0) { + Log.i(TAG,"Heart Rate Fault (HR<0)"); + mSdData.mHRFaultStanding = true; + mSdData.mHRAlarmStanding = false; + } + else if ((mSdData.mHR > mSdData.mHRTreshMax) || (mSdData.mHR < mSdData.mHRThreshMin)) { + Log.i(TAG, "Heart Rate Abnormal - " + mSdData.mHR + " bpm"); + mSdData.mHRFaultStanding = false; + mSdData.mHRAlarmStanding = true; + } + else { + mSdData.mHRFaultStanding = false; + mSdData.mHRAlarmStanding = false; + } + } + + } + /**************************************************************** * Simple threshold analysis to chech for fall. * Called from clock_tick_handler() */ - public void checkFall() { + public void fallCheck() { int i,j; double minAcc, maxAcc; @@ -575,7 +653,7 @@ public class SdDataSourceGarmin extends SdDataSource { Log.v(TAG, "getStatus() - mWatchAppRunningCheck=" + mWatchAppRunningCheck + " tdiff=" + tdiff); Log.v(TAG,"getStatus() - tdiff="+tdiff+", mDataUpatePeriod="+mDataUpdatePeriod+", mAppRestartTimeout="+mAppRestartTimeout); - mSdData.pebbleConnected = true; // We can't check connection for passive network connection, so set it to true to avoid errors. + mSdData.watchConnected = true; // We can't check connection for passive network connection, so set it to true to avoid errors. // And is the watch app running? // set mWatchAppRunningCheck has been false for more than 10 seconds // the app is not talking to us @@ -583,7 +661,7 @@ public class SdDataSourceGarmin extends SdDataSource { if (!mWatchAppRunningCheck && (tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) { Log.v(TAG, "getStatus() - tdiff = " + tdiff); - mSdData.pebbleAppRunning = false; + mSdData.watchAppRunning = false; // Only make audible warning beep if we have not received data for more than mFaultTimerPeriod seconds. if (tdiff > (mDataUpdatePeriod + mFaultTimerPeriod) * 1000) { Log.v(TAG, "getStatus() - Watch App Not Running"); @@ -596,7 +674,7 @@ public class SdDataSourceGarmin extends SdDataSource { Log.v(TAG, "getStatus() - Waiting for mFaultTimerPeriod before issuing audible warning..."); } } else { - mSdData.pebbleAppRunning = true; + mSdData.watchAppRunning = true; } // if we have confirmation that the app is running, reset the @@ -612,71 +690,21 @@ public class SdDataSourceGarmin extends SdDataSource { } /** - * alarmCheck - determines alarm state based on seizure detector data SdData. Called every second. + * faultCheck - determines alarm state based on seizure detector data SdData. Called every second. */ - private void alarmCheck() { - boolean inAlarm; + private void faultCheck() { Time tnow = new Time(Time.getCurrentTimezone()); long tdiff; tnow.setToNow(); // get time since the last data was received from the watch. tdiff = (tnow.toMillis(false) - mDataStatusTime.toMillis(false)); - Log.v(TAG, "alarmCheck() - tdiff=" + tdiff + ", mDataUpatePeriod=" + mDataUpdatePeriod + ", mAppRestartTimeout=" + mAppRestartTimeout + Log.v(TAG, "faultCheck() - tdiff=" + tdiff + ", mDataUpatePeriod=" + mDataUpdatePeriod + ", mAppRestartTimeout=" + mAppRestartTimeout + ", combined = " + (mDataUpdatePeriod + mAppRestartTimeout) * 1000); if (!mWatchAppRunningCheck && (tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) { - Log.v(TAG, "alarmCheck() - watch app not running so not doing anything"); + Log.v(TAG, "faultCheck() - watch app not running so not doing anything"); mAlarmCount = 0; - } else { - Log.v(TAG, "alarmCheck()"); - if ((mSdData.roiPower > mAlarmThresh) && (10 * (mSdData.roiPower / mSdData.specPower) > mAlarmRatioThresh)) { - inAlarm = true; - } else { - inAlarm = false; - } - - if (inAlarm) { - mAlarmCount += mSamplePeriod; - if (mAlarmCount > mAlarmTime) { - // full alarm - mSdData.alarmState = 2; - } else if (mAlarmCount > mWarnTime) { - // warning - mSdData.alarmState = 1; - } - } else { - // If we are not in an ALARM state, revert back to WARNING, otherwise - // revert back to OK. - if (mSdData.alarmState == 2) { - // revert to warning - mSdData.alarmState = 1; - mAlarmCount = mWarnTime + 1; // pretend we have only just entered warning state. - } else { - // revert to OK - mSdData.alarmState = 0; - mAlarmCount = 0; - } - } - - Log.v(TAG, "inAlarm=" + inAlarm + ", alarmState = " + mSdData.alarmState + " alarmCount=" + mAlarmCount + " mAlarmTime=" + mAlarmTime); - - /* Check Heart Rate against alarm settings */ - if (mSdData.mHRAlarmActive) { - Log.v(TAG,"Checking HR Alarm"); - if ((mSdData.mHR > mSdData.mHRTreshMax) || (mSdData.mHR < mSdData.mHRThreshMin)) { - Log.i(TAG, "Heart Rate Abnormal - " + mSdData.mHR + " bpm"); - mSdData.mHRAlarmStanding = true; - } else { - mSdData.mHRAlarmStanding = false; - } - } - if (mMute != 0) { - Log.v(TAG,"Mute Active - setting alarms to mute"); - mSdData.alarmState = 6; - mSdData.alarmPhrase = "MUTE"; - mSdData.mHRAlarmStanding = false; - } } } diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceNetwork.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceNetwork.java index 12d776f..99fbf25 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceNetwork.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceNetwork.java @@ -2,8 +2,6 @@ package uk.org.openseizuredetector; import android.content.Context; import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Handler; import android.preference.PreferenceManager; @@ -128,8 +126,8 @@ public class SdDataSourceNetwork extends SdDataSource { if (result.startsWith("Unable to retrieve web page")) { Log.v(TAG,"doInBackground() - Unable to retrieve data"); sdData.serverOK = false; - sdData.pebbleConnected = false; - sdData.pebbleAppRunning = false; + sdData.watchConnected = false; + sdData.watchAppRunning = false; sdData.alarmState = ALARM_STATE_NETFAULT; sdData.alarmPhrase = "Warning - No Connection to Server"; Log.v(TAG,"doInBackground(): No Connection to Server - sdData = "+sdData.toString()); @@ -147,8 +145,8 @@ public class SdDataSourceNetwork extends SdDataSource { } catch (IOException e) { sdData.serverOK = false; - sdData.pebbleConnected = false; - sdData.pebbleAppRunning = false; + sdData.watchConnected = false; + sdData.watchAppRunning = false; sdData.alarmState = ALARM_STATE_NETFAULT; sdData.alarmPhrase = "Warning - No Connection to Server"; Log.v(TAG,"doInBackground(): IOException - "+e.toString()); diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java index ed2b109..10ef90f 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java @@ -37,11 +37,6 @@ import android.widget.Toast; import com.getpebble.android.kit.PebbleKit; import com.getpebble.android.kit.util.PebbleDictionary; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; @@ -679,8 +674,8 @@ public class SdDataSourcePebble extends SdDataSource { tdiff = (tnow.toMillis(false) - mPebbleStatusTime.toMillis(false)); Log.v(TAG, "getStatus() - mPebbleAppRunningCheck=" + mPebbleAppRunningCheck + " tdiff=" + tdiff); // Check we are actually connected to the pebble. - mSdData.pebbleConnected = PebbleKit.isWatchConnected(mContext); - if (!mSdData.pebbleConnected) mPebbleAppRunningCheck = false; + mSdData.watchConnected = PebbleKit.isWatchConnected(mContext); + if (!mSdData.watchConnected) mPebbleAppRunningCheck = false; // And is the pebble_sd app running? // set mPebbleAppRunningCheck has been false for more than 10 seconds // the app is not talking to us @@ -688,7 +683,7 @@ public class SdDataSourcePebble extends SdDataSource { if (!mPebbleAppRunningCheck && (tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) { Log.v(TAG, "getStatus() - tdiff = " + tdiff); - mSdData.pebbleAppRunning = false; + mSdData.watchAppRunning = false; //Log.v(TAG, "getStatus() - Pebble App Not Running - Attempting to Re-Start"); //mUtil.writeToSysLogFile("SdDataSourcePebble.getStatus() - Pebble App not Running - Attempting to Re-Start"); //startWatchApp(); @@ -705,7 +700,7 @@ public class SdDataSourcePebble extends SdDataSource { Log.v(TAG, "getStatus() - Waiting for mFaultTimerPeriod before issuing audible warning..."); } } else { - mSdData.pebbleAppRunning = true; + mSdData.watchAppRunning = true; } // if we have confirmation that the app is running, reset the diff --git a/app/src/main/java/uk/org/openseizuredetector/SdServer.java b/app/src/main/java/uk/org/openseizuredetector/SdServer.java index 4cb15b0..385e922 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdServer.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdServer.java @@ -629,7 +629,7 @@ public class SdServer extends Service implements SdDataReceiver { } // Fault - if ((sdData.alarmState) == 4 || (sdData.alarmState == 7)) { + if ((sdData.alarmState) == 4 || (sdData.alarmState == 7) || (sdData.mHRFaultStanding)) { sdData.alarmPhrase = "FAULT"; writeAlarmToSD(); faultWarningBeep(); diff --git a/app/src/main/java/uk/org/openseizuredetector/SdServiceConnection.java b/app/src/main/java/uk/org/openseizuredetector/SdServiceConnection.java index b2e9b2c..0e4b817 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdServiceConnection.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdServiceConnection.java @@ -101,7 +101,7 @@ public class SdServiceConnection implements ServiceConnection { public boolean pebbleConnected() { if (mSdServer!=null) { if (mSdServer.mSdData!=null) { - if (mSdServer.mSdData.pebbleConnected) { + if (mSdServer.mSdData.watchConnected) { return true; } } @@ -116,7 +116,7 @@ public class SdServiceConnection implements ServiceConnection { public boolean pebbleAppRunning() { if (mSdServer!=null) { if (mSdServer.mSdData!=null) { - if (mSdServer.mSdData.pebbleAppRunning) { + if (mSdServer.mSdData.watchAppRunning) { return true; } } diff --git a/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java b/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java index bff1caa..059b553 100644 --- a/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java @@ -340,7 +340,7 @@ public class StartupActivity extends Activity { // Is Pebble Watch App Running? tv = (TextView) findViewById(R.id.textItem4); pb = (ProgressBar) findViewById(R.id.progressBar4); - if (mConnection.pebbleAppRunning()) { + if (mConnection.watchAppRunning()) { tv.setText("Watch App Running OK"); tv.setBackgroundColor(okColour); tv.setTextColor(okTextColour); @@ -506,6 +506,7 @@ public class StartupActivity extends Activity { + "\n V3.1.5 - Added repeat alarm beeps during SMS delay to alert user.." + "\n V3.1.6 - Made Cancel Audible button inhibit sending SMS alarms as well as audible beeps" + "\n V3.1.8 - Added READ_PHONE_STATE permission, which seems to be needed for some phones" + + "\n V3.1.9 - Fixed issue with Garmin Seizure Detector not producing warnings. Added faut pips for missing heart rate data if heart rate alarm active" ); // This makes the links display as links, but they do not respond to clicks for some reason... Linkify.addLinks(s, Linkify.ALL); @@ -540,6 +541,7 @@ public class StartupActivity extends Activity { + "\n V3.1.5 - Added repeat alarm beeps during SMS delay to alert user.." + "\n V3.1.6 - Made Cancel Audible button inhibit sending SMS alarms as well as audible beeps" + "\n V3.1.8 - Added READ_PHONE_STATE permission, which seems to be needed for some phones" + + "\n V3.1.9 - Fixed issue with Garmin Seizure Detector not producing warnings. Added faut pips for missing heart rate data if heart rate alarm active" + "\n " ); // This makes the links display as links, but they do not respond to clicks for some reason... diff --git a/app/src/test/java/uk/org/openseizuredetector/EventLogManagerTest.java b/app/src/test/java/uk/org/openseizuredetector/EventLogManagerTest.java deleted file mode 100644 index faca38e..0000000 --- a/app/src/test/java/uk/org/openseizuredetector/EventLogManagerTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package uk.org.openseizuredetector; - -import android.content.Context; -import android.util.EventLog; -import android.util.Log; - -import junit.framework.TestCase; -import android.test.mock.MockContext; - -import org.junit.Test; -import org.mockito.internal.exceptions.ExceptionIncludingMockitoWarnings; - -import uk.org.openseizuredetector.EventLogManager.EventLogManager; -import uk.org.openseizuredetector.EventLogManager.LogEntryModel; - -/** - * Created by graham on 12/05/16. - */ -public class EventLogManagerTest extends TestCase { - private final static String TAG = "EventLogManagerTest"; - Context mContext; - - protected void setUp() throws Exception { - super.setUp(); - Log.v(TAG,"setUp()"); - mContext = new MockContext(); -} - - @Test - public void testOpenDb() throws Exception { - Log.v(TAG,"testOpenDb()"); - EventLogManager em = new EventLogManager(mContext); - assertNotNull(em); - - - LogEntryModel lem = new LogEntryModel(); - //lem.setDate(new Date()); - lem.setNote("Test Entry"); - lem.setDataJSON("[]"); - lem.setAlarmState(1); - - em.addRow(lem); - - } - -} diff --git a/app/src/test/java/uk/org/openseizuredetector/SdAnalyserTest.java b/app/src/test/java/uk/org/openseizuredetector/SdAnalyserTest.java new file mode 100644 index 0000000..35b483e --- /dev/null +++ b/app/src/test/java/uk/org/openseizuredetector/SdAnalyserTest.java @@ -0,0 +1,90 @@ +package uk.org.openseizuredetector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SdAnalyserTest { + public SdAnalyser sda; + + public String alarmJSON = "{ dataType: 'raw', " + + "data: [" + + "1644, 1316, 1144, 1332, 1716, 1716, 1392, 1148, 1276, 1660, " + + "1716, 1496, 1196, 1232, 1572, 1684, 1552, 1236, 1228, 1528, " + + "1648, 1572, 1268, 1208, 1492, 1680, 1596, 1272, 1192, 1424, " + + "1668, 1636, 1300, 1200, 1356, 1652, 1684, 1420, 1208, 1304, " + + "1620, 1672, 1448, 1232, 1248, 1536, 1676, 1540, 1256, 1244, " + + "1544, 1644, 1512, 1252, 1236, 1504, 1684, 1540, 1252, 1200, " + + "1436, 1664, 1624, 1344, 1204, 1396, 1616, 1596, 1344, 1216, " + + "1368, 1648, 1660, 1388, 1220, 1316, 1588, 1672, 1460, 1232, " + + "1256, 1580, 1672, 1500, 1256, 1288, 1540, 1688, 1516, 1252, " + + "1212, 1464, 1684, 1584, 1288, 1224, 1468, 1692, 1616, 1316, " + + "1188, 1360, 1680, 1724, 1424, 1192, 1224, 1556, 1744, 1588, " + + "1260, 1220, 1472, 1692, 1608, 1328, 1192, 1412, 1668, 1656, " + + "1356, 1216, 1304, 1636, 1712], " + + "HR:54, " + + "Mute:0 " + + "}"; + + private String okJSON = "{ " + + "dataType: 'raw', " + + "data: [" + + "1140, 1188, 1144, 1172, 1228, 1212, 1236, 1236, 1256, 1320, " + + "1316, 1280, 1240, 1280, 1324, 1284, 1292, 1268, 1284, 1276, " + + "1296, 1324, 1308, 1288, 1304, 1276, 1304, 1304, 1276, 1296, " + + "1280, 1284, 1296, 1300, 1284, 1288, 1296, 1284, 1300, 1280, " + + "1300, 1292, 1276, 1304, 1276, 1316, 1280, 1288, 1296, 1280, " + + "1284, 1272, 1300, 1284, 1288, 1292, 1276, 1296, 1276, 1292, " + + "1280, 1284, 1284, 1284, 1284, 1284, 1288, 1284, 1304, 1284, " + + "1288, 1280, 1296, 1284, 1292, 1296, 1280, 1276, 1288, 1296, " + + "1276, 1292, 1288, 1276, 1288, 1276, 1272, 1272, 1292, 1284, " + + "1292, 1288, 1280, 1284, 1284, 1268, 1288, 1268, 1276, 1300, " + + "1268, 1292, 1292, 1304, 1288, 1284, 1280, 1276, 1288, 1280, " + + "1300, 1288, 1320, 1268, 1288, 1280, 1304, 1280, 1280, 1288, " + + "1292, 1308, 1268, 1292, 1280], " + + "HR:57, " + + "Mute:0 " + + "}"; + + @Before + public void setUp() throws Exception { + sda = new SdAnalyser(25.0, + 3.0, + 8.0, + 5.0, + 5.0, + 100.0, + 54.0 + ); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void freq2fftBin() { + int n; + n = sda.freq2fftBin(0.0); + assertEquals(0,n); + n = sda.freq2fftBin(5.0); + assertEquals(25,n); + } + + @Test + public void getMagnitude() { + double[] fft = {1, 1, + 2, 1, + 2, 2}; + double m; + m = sda.getMagnitude(fft,0); + assertEquals(2.0, m, m * 1e-4); + m = sda.getMagnitude(fft,1); + assertEquals(5.0, m, m * 1e-4); + m = sda.getMagnitude(fft,2); + assertEquals(8.0, m, m * 1e-4); + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index f04c4c9..e86444e 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:3.4.1' } } allprojects {