diff --git a/app/build.gradle b/app/build.gradle index 944795a..81c2bfd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,17 +24,16 @@ android { dependencies { compile files('libs/mpandroidchartlibrary-2-0-7.jar') - compile 'com.getpebble:pebblekit:3.0.0@aar' - + compile 'com.getpebble:pebblekit:3.1.0@aar' // Unit testing dependencies testCompile 'junit:junit:4.12' // Set this dependency if you want to use Mockito testCompile 'org.mockito:mockito-core:1.10.19' // Set this dependency if you want to use Hamcrest matching testCompile 'org.hamcrest:hamcrest-library:1.1' - compile 'com.android.support:appcompat-v7:22.2.1' compile 'com.android.support:support-v4:22.2.1' + compile files('libs/JTransforms-3.1-with-dependencies.jar') } repositories { diff --git a/app/libs/JTransforms-3.1-with-dependencies.jar b/app/libs/JTransforms-3.1-with-dependencies.jar new file mode 100644 index 0000000..c6d5e1a Binary files /dev/null and b/app/libs/JTransforms-3.1-with-dependencies.jar differ diff --git a/app/src/main/assets/pebble_sd.pbw b/app/src/main/assets/pebble_sd.pbw index b31526c..b4679f9 100644 Binary files a/app/src/main/assets/pebble_sd.pbw and b/app/src/main/assets/pebble_sd.pbw differ diff --git a/app/src/main/java/uk/org/openseizuredetector/AccelData.java b/app/src/main/java/uk/org/openseizuredetector/AccelData.java new file mode 100644 index 0000000..2eabed3 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/AccelData.java @@ -0,0 +1,85 @@ +package uk.org.openseizuredetector; + +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TimeZone; + +/** + * Created by graham on 27/06/16. + */ +/* From https://github.com/kramimus/pebble-accel-analyzer */ +public class AccelData { + private final String TAG = AccelData.class.getSimpleName(); + + final private int x; + final private int y; + final private int z; + + private long timestamp = 0; + final private boolean didVibrate; + + public AccelData(byte[] data) { + x = (data[0] & 0xff) | (data[1] << 8); + y = (data[2] & 0xff) | (data[3] << 8); + z = (data[4] & 0xff) | (data[5] << 8); + didVibrate = data[6] != 0; + + for (int i = 0; i < 8; i++) { + timestamp |= ((long)(data[i+7] & 0xff)) << (i * 8); + } + } + + public JSONObject toJson() { + JSONObject json = new JSONObject(); + try { + json.put("x", x); + json.put("y", y); + json.put("z", z); + json.put("ts", timestamp); + json.put("v", didVibrate); + return json; + } catch (JSONException e) { + Log.w(TAG, "problem constructing accel data, skipping " + e); + } + return null; + } + + public static List fromDataArray(byte[] data) { + List accels = new ArrayList(); + for (int i = 0; i < data.length; i += 15) { + accels.add(new AccelData(Arrays.copyOfRange(data, i, i + 15))); + } + return accels; + } + + public long getTimestamp() { + return timestamp; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getZ() { + return z; + } + + public int getMagnitude() { + return (int)Math.sqrt(x*x + y*y + z*z); + } + + public void applyTimezone(TimeZone tz) { + timestamp -= tz.getOffset(timestamp); + } +} + diff --git a/app/src/main/java/uk/org/openseizuredetector/CircularArrayList.java b/app/src/main/java/uk/org/openseizuredetector/CircularArrayList.java new file mode 100644 index 0000000..08d7d10 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/CircularArrayList.java @@ -0,0 +1,107 @@ +package uk.org.openseizuredetector; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.RandomAccess; + +/** + * Created by graham on 28/06/16. + */ +public class CircularArrayList + extends AbstractList implements RandomAccess { + /** + * If you use this code, please consider notifying isak at du-preez dot com + * with a brief description of your application. + * + * This is free and unencumbered software released into the public domain. + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any + * means. + */ + + private final int n; // buffer length + private final List buf; // a List implementing RandomAccess + private int head = 0; + private int tail = 0; + + public CircularArrayList(int capacity) { + n = capacity + 1; + buf = new ArrayList(Collections.nCopies(n, (E) null)); + } + + public int capacity() { + return n - 1; + } + + private int wrapIndex(int i) { + int m = i % n; + if (m < 0) { // java modulus can be negative + m += n; + } + return m; + } + + // This method is O(n) but will never be called if the + // CircularArrayList is used in its typical/intended role. + private void shiftBlock(int startIndex, int endIndex) { + assert (endIndex > startIndex); + for (int i = endIndex - 1; i >= startIndex; i--) { + set(i + 1, get(i)); + } + } + + @Override + public int size() { + return tail - head + (tail < head ? n : 0); + } + + @Override + public E get(int i) { + if (i < 0 || i >= size()) { + throw new IndexOutOfBoundsException(); + } + return buf.get(wrapIndex(head + i)); + } + + @Override + public E set(int i, E e) { + if (i < 0 || i >= size()) { + throw new IndexOutOfBoundsException(); + } + return buf.set(wrapIndex(head + i), e); + } + + @Override + public void add(int i, E e) { + int s = size(); + if (s == n - 1) { + throw new IllegalStateException("Cannot add element." + + " CircularArrayList is filled to capacity."); + } + if (i < 0 || i > s) { + throw new IndexOutOfBoundsException(); + } + tail = wrapIndex(tail + 1); + if (i < s) { + shiftBlock(i, s); + } + set(i, e); + } + + @Override + public E remove(int i) { + int s = size(); + if (i < 0 || i >= s) { + throw new IndexOutOfBoundsException(); + } + E e = get(i); + if (i > 0) { + shiftBlock(0, i); + } + head = wrapIndex(head + 1); + return e; + } +} diff --git a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java index e7b5d6e..77e46ea 100644 --- a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java +++ b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java @@ -42,7 +42,12 @@ import org.apache.http.conn.util.InetAddressUtils; import java.net.InetAddress; import java.net.NetworkInterface; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; +import java.util.List; +import java.util.RandomAccess; /** * OsdUtil - OpenSeizureDetector Utilities diff --git a/app/src/main/java/uk/org/openseizuredetector/SdData.java b/app/src/main/java/uk/org/openseizuredetector/SdData.java index 3959504..30f3e56 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdData.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdData.java @@ -50,6 +50,8 @@ public class SdData implements Parcelable { public short mFallThreshMin; public short mFallThreshMax; public short mFallWindow; + public long mSdMode; + public long mSampleFreq; public long alarmFreqMin; public long alarmFreqMax; public long nMin; @@ -148,6 +150,8 @@ public class SdData implements Parcelable { jsonObj.put("haveSettings", haveSettings); jsonObj.put("alarmState", alarmState); jsonObj.put("alarmPhrase", alarmPhrase); + jsonObj.put("sdMode",mSdMode); + jsonObj.put("sampleFreq",mSampleFreq); jsonObj.put("alarmFreqMin",alarmFreqMin); jsonObj.put("alarmFreqMax",alarmFreqMax); jsonObj.put("alarmThresh", alarmThresh); diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java index 3fd64f2..0a9d53b 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePebble.java @@ -38,6 +38,10 @@ import com.getpebble.android.kit.Constants; import com.getpebble.android.kit.PebbleKit; import com.getpebble.android.kit.util.PebbleDictionary; +import org.json.JSONException; +import org.json.JSONObject; +import org.jtransforms.fft.DoubleFFT_1D; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -46,6 +50,10 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; @@ -63,7 +71,7 @@ public class SdDataSourcePebble extends SdDataSource { private Time mPebbleStatusTime; private boolean mPebbleAppRunningCheck = false; private int mAppRestartTimeout = 10; // Timeout before re-starting watch app (sec) if we have not received - // data after mDataUpdatePeriod + // data after mDataUpdatePeriod //private Looper mServiceLooper; private int mFaultTimerPeriod = 30; // Fault Timer Period in sec private PebbleKit.PebbleDataReceiver msgDataHandler = null; @@ -101,14 +109,27 @@ public class SdDataSourcePebble extends SdDataSource { private int KEY_DATA_UPDATE_PERIOD = 25; private int KEY_MUTE_PERIOD = 26; private int KEY_MAN_ALARM_PERIOD = 27; + private int KEY_SD_MODE = 28; + private int KEY_SAMPLE_FREQ = 29; + private int KEY_RAW_DATA = 30; + private int KEY_NUM_RAW_DATA = 31; // Values of the KEY_DATA_TYPE entry in a message private int DATA_TYPE_RESULTS = 1; // Analysis Results private int DATA_TYPE_SETTINGS = 2; // Settings private int DATA_TYPE_SPEC = 3; // FFT Spectrum (or part of a spectrum) + private int DATA_TYPE_RAW = 4; // raw accelerometer data. + + // Values for SD_MODE + private int SD_MODE_FFT = 0; // The original OpenSeizureDetector mode (FFT based) + private int SD_MODE_RAW = 1; // Send raw, unprocessed data to the phone. + private int SD_MODE_FILTER = 2; // Use digital filter rather than FFT. + private short mDataUpdatePeriod; private short mMutePeriod; private short mManAlarmPeriod; + private short mPebbleSdMode; + private short mSampleFreq; private short mAlarmFreqMin; private short mAlarmFreqMax; private short mWarnTime; @@ -120,8 +141,13 @@ public class SdDataSourcePebble extends SdDataSource { private short mFallThreshMax; private short mFallWindow; + // raw data storage for SD_MODE_RAW + private int MAX_RAW_DATA = 500; + private double[] rawData = new double[MAX_RAW_DATA]; + private int nRawData = 0; + public SdDataSourcePebble(Context context, SdDataReceiver sdDataReceiver) { - super(context,sdDataReceiver); + super(context, sdDataReceiver); mName = "Pebble"; // Set default settings from XML files (mContext is set by super(). PreferenceManager.setDefaultValues(mContext, @@ -251,7 +277,15 @@ public class SdDataSourcePebble extends SdDataSource { mManAlarmPeriod = (short) Integer.parseInt(prefStr); Log.v(TAG, "updatePrefs() ManAlarmPeriod = " + mManAlarmPeriod); - prefStr = SP.getString("AlarmFreqMin","SET_FROM_XML"); + prefStr = SP.getString("PebbleSdMode", "SET_FROM_XML"); + mPebbleSdMode = (short) Integer.parseInt(prefStr); + Log.v(TAG, "updatePrefs() PebbleSdMode = " + mPebbleSdMode); + + prefStr = SP.getString("SampleFreq", "SET_FROM_XML"); + mSampleFreq = (short) Integer.parseInt(prefStr); + Log.v(TAG, "updatePrefs() SampleFreq = " + mSampleFreq); + + prefStr = SP.getString("AlarmFreqMin", "SET_FROM_XML"); mAlarmFreqMin = (short) Integer.parseInt(prefStr); Log.v(TAG, "updatePrefs() AlarmFreqMin = " + mAlarmFreqMin); @@ -313,7 +347,7 @@ public class SdDataSourcePebble extends SdDataSource { Log.v(TAG, "Received message from Pebble - data type=" + data.getUnsignedIntegerAsLong(KEY_DATA_TYPE)); // If we have a message, the app must be running - Log.v(TAG,"Setting mPebbleAppRunningCheck to true"); + Log.v(TAG, "Setting mPebbleAppRunningCheck to true"); mPebbleAppRunningCheck = true; PebbleKit.sendAckToPebble(context, transactionId); //Log.v(TAG,"Message is: "+data.toJsonString()); @@ -365,6 +399,23 @@ public class SdDataSourcePebble extends SdDataSource { mSdData.batteryPc = data.getUnsignedIntegerAsLong(KEY_BATTERY_PC); mSdData.haveSettings = true; } + if (data.getUnsignedIntegerAsLong(KEY_DATA_TYPE) + == DATA_TYPE_RAW) { + Log.v(TAG, "DATA_TYPE = Raw"); + long numSamples; + numSamples = data.getUnsignedIntegerAsLong(KEY_NUM_RAW_DATA); + Log.v(TAG, "numSamples = " + numSamples); + byte[] rawDataBytes = data.getBytes(KEY_RAW_DATA); + for (AccelData reading : AccelData.fromDataArray(rawDataBytes)) { + if (nRawData < MAX_RAW_DATA) { + rawData[nRawData] = reading.getMagnitude(); + nRawData++; + } else { + Log.i(TAG, "WARNING - rawData Buffer Full"); + } + } + + } } }; PebbleKit.registerReceivedDataHandler(mContext, msgDataHandler); @@ -427,12 +478,14 @@ public class SdDataSourcePebble extends SdDataSource { * variables to the watch. */ public void sendPebbleSdSettings() { - Log.v(TAG, "sendPebblSdSettings() - preparing settings dictionary.."); + Log.v(TAG, "sendPebblSdSettings() - preparing settings dictionary.. mSampleFreq=" + mSampleFreq); // Watch Settings final PebbleDictionary setDict = new PebbleDictionary(); setDict.addInt16(KEY_DATA_UPDATE_PERIOD, mDataUpdatePeriod); setDict.addInt16(KEY_MUTE_PERIOD, mMutePeriod); setDict.addInt16(KEY_MAN_ALARM_PERIOD, mManAlarmPeriod); + setDict.addInt16(KEY_SD_MODE, mPebbleSdMode); + setDict.addInt16(KEY_SAMPLE_FREQ, mSampleFreq); setDict.addInt16(KEY_ALARM_FREQ_MIN, mAlarmFreqMin); setDict.addInt16(KEY_ALARM_FREQ_MAX, mAlarmFreqMax); setDict.addUint16(KEY_WARN_TIME, mWarnTime); @@ -460,57 +513,57 @@ public class SdDataSourcePebble extends SdDataSource { * @return true if they are all the same, or false if there are discrepancies. */ public boolean checkWatchSettings() { - boolean settingsOk = true; - if (mDataUpdatePeriod != mSdData.mDataUpdatePeriod) { - Log.v(TAG,"checkWatchSettings - mDataUpdatePeriod Wrong"); + boolean settingsOk = true; + if (mDataUpdatePeriod != mSdData.mDataUpdatePeriod) { + Log.v(TAG, "checkWatchSettings - mDataUpdatePeriod Wrong"); settingsOk = false; } if (mMutePeriod != mSdData.mMutePeriod) { - Log.v(TAG,"checkWatchSettings - mMutePeriod Wrong"); + Log.v(TAG, "checkWatchSettings - mMutePeriod Wrong"); settingsOk = false; } if (mManAlarmPeriod != mSdData.mManAlarmPeriod) { - Log.v(TAG,"checkWatchSettings - mManAlarmPeriod Wrong"); + Log.v(TAG, "checkWatchSettings - mManAlarmPeriod Wrong"); settingsOk = false; } if (mAlarmFreqMin != mSdData.alarmFreqMin) { - Log.v(TAG,"checkWatchSettings - mAlarmFreqMin Wrong"); + Log.v(TAG, "checkWatchSettings - mAlarmFreqMin Wrong"); settingsOk = false; } if (mAlarmFreqMax != mSdData.alarmFreqMax) { - Log.v(TAG,"checkWatchSettings - mAlarmFreqMax Wrong"); + Log.v(TAG, "checkWatchSettings - mAlarmFreqMax Wrong"); settingsOk = false; } if (mWarnTime != mSdData.warnTime) { - Log.v(TAG,"checkWatchSettings - mWarnTime Wrong"); + Log.v(TAG, "checkWatchSettings - mWarnTime Wrong"); settingsOk = false; } if (mAlarmTime != mSdData.alarmTime) { - Log.v(TAG,"checkWatchSettings - mAlarmTime Wrong"); + Log.v(TAG, "checkWatchSettings - mAlarmTime Wrong"); settingsOk = false; } if (mAlarmThresh != mSdData.alarmThresh) { - Log.v(TAG,"checkWatchSettings - mAlarmThresh Wrong"); + Log.v(TAG, "checkWatchSettings - mAlarmThresh Wrong"); settingsOk = false; } if (mAlarmRatioThresh != mSdData.alarmRatioThresh) { - Log.v(TAG,"checkWatchSettings - mAlarmRatioThresh Wrong"); + Log.v(TAG, "checkWatchSettings - mAlarmRatioThresh Wrong"); settingsOk = false; } if (mFallActive != mSdData.mFallActive) { - Log.v(TAG,"checkWatchSettings - mAlarmFreqMin Wrong"); + Log.v(TAG, "checkWatchSettings - mAlarmFreqMin Wrong"); settingsOk = false; } if (mFallThreshMin != mSdData.mFallThreshMin) { - Log.v(TAG,"checkWatchSettings - mFallThreshMin Wrong"); + Log.v(TAG, "checkWatchSettings - mFallThreshMin Wrong"); settingsOk = false; } if (mFallThreshMax != mSdData.mFallThreshMax) { - Log.v(TAG,"checkWatchSettings - mFallThreshMax Wrong"); + Log.v(TAG, "checkWatchSettings - mFallThreshMax Wrong"); settingsOk = false; } if (mFallWindow != mSdData.mFallWindow) { - Log.v(TAG,"checkWatchSettings - mFallWindow Wrong"); + Log.v(TAG, "checkWatchSettings - mFallWindow Wrong"); settingsOk = false; } @@ -543,7 +596,7 @@ public class SdDataSourcePebble extends SdDataSource { tnow.setToNow(); // get time since the last data was received from the Pebble watch. tdiff = (tnow.toMillis(false) - mPebbleStatusTime.toMillis(false)); - Log.v(TAG, "getPebbleStatus() - mPebbleAppRunningCheck="+mPebbleAppRunningCheck+" tdiff="+tdiff); + Log.v(TAG, "getPebbleStatus() - mPebbleAppRunningCheck=" + mPebbleAppRunningCheck + " tdiff=" + tdiff); // Check we are actually connected to the pebble. mSdData.pebbleConnected = PebbleKit.isWatchConnected(mContext); if (!mSdData.pebbleConnected) mPebbleAppRunningCheck = false; @@ -552,7 +605,7 @@ public class SdDataSourcePebble extends SdDataSource { // the app is not talking to us // mPebbleAppRunningCheck is set to true in the receiveData handler. if (!mPebbleAppRunningCheck && - (tdiff > (mDataUpdatePeriod+mAppRestartTimeout) * 1000)) { + (tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) { Log.v(TAG, "getPebbleStatus() - tdiff = " + tdiff); mSdData.pebbleAppRunning = false; Log.v(TAG, "getPebbleStatus() - Pebble App Not Running - Attempting to Re-Start"); @@ -560,7 +613,7 @@ public class SdDataSourcePebble extends SdDataSource { //mPebbleStatusTime = tnow; // set status time to now so we do not re-start app repeatedly. getPebbleSdSettings(); // Only make audible warning beep if we have not received data for more than mFaultTimerPeriod seconds. - if (tdiff > (mDataUpdatePeriod+mFaultTimerPeriod) * 1000) { + if (tdiff > (mDataUpdatePeriod + mFaultTimerPeriod) * 1000) { mSdDataReceiver.onSdDataFault(mSdData); } else { Log.v(TAG, "getPebbleStatus() - Waiting for mFaultTimerPeriod before issuing audible warning..."); @@ -581,6 +634,19 @@ public class SdDataSourcePebble extends SdDataSource { getPebbleSdSettings(); getPebbleData(); } + + if (mPebbleSdMode == SD_MODE_RAW) { + analyseRawData(); + } + } + + + private void analyseRawData() { + Log.v(TAG,"analyserawData()"); + DoubleFFT_1D fft = new DoubleFFT_1D(MAX_RAW_DATA); + fft.realForward(rawData); + // FIXME - rawData should really be a circular buffer. + nRawData = 0; } /** @@ -614,10 +680,9 @@ public class SdDataSourcePebble extends SdDataSource { } } - - - - - } + + + + diff --git a/app/src/main/res/values/pebble_sample_freq_list.xml b/app/src/main/res/values/pebble_sample_freq_list.xml new file mode 100644 index 0000000..7ffcea3 --- /dev/null +++ b/app/src/main/res/values/pebble_sample_freq_list.xml @@ -0,0 +1,16 @@ + + + + "100 Hz" + "50 Hz" + "25 Hz" + "10 Hz" + + + "100" + "50" + "25" + "10" + + + \ No newline at end of file diff --git a/app/src/main/res/values/pebble_sd_mode_list.xml b/app/src/main/res/values/pebble_sd_mode_list.xml new file mode 100644 index 0000000..130121d --- /dev/null +++ b/app/src/main/res/values/pebble_sd_mode_list.xml @@ -0,0 +1,14 @@ + + + + "Normal - OpenSeizureDetector FFT" + "Raw" + "Digital Filter" + + + "0" + "1" + "2" + + + \ No newline at end of file diff --git a/app/src/main/res/xml/general_prefs.xml b/app/src/main/res/xml/general_prefs.xml index 642117c..a9f7381 100644 --- a/app/src/main/res/xml/general_prefs.xml +++ b/app/src/main/res/xml/general_prefs.xml @@ -6,6 +6,7 @@ android:summary="Select whether to use a Pebble Watch or network connection as the seizure detector data source." android:entries="@array/datasource_list" android:entryValues="@array/datasource_list_values" + android:defaultValue="Pebble" android:dialogTitle="Select Data Source" /> + + +