diff --git a/android/ConductorsPhone/app/build.gradle b/android/ConductorsPhone/app/build.gradle index 550f728..9d86821 100644 --- a/android/ConductorsPhone/app/build.gradle +++ b/android/ConductorsPhone/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 24 targetSdkVersion 26 //sdk 2 | product version 3 | build num 2 | multi-apk 2 - versionCode 260130300 - versionName "0.1.3.2" + versionCode 260130400 + versionName "0.1.3.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/android/ConductorsSensor/.gitignore b/android/ConductorsSensor/.gitignore new file mode 100644 index 0000000..5edb4ee --- /dev/null +++ b/android/ConductorsSensor/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/android/ConductorsSensor/app/.gitignore b/android/ConductorsSensor/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/android/ConductorsSensor/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/ConductorsSensor/app/build.gradle b/android/ConductorsSensor/app/build.gradle new file mode 100644 index 0000000..8b1a7ce --- /dev/null +++ b/android/ConductorsSensor/app/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 26 + defaultConfig { + applicationId "de.tonifetzer.conductorssensor" + minSdkVersion 21 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildToolsVersion '27.0.3' +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + api 'com.mbientlab:metawear:3.4.0' + implementation 'com.android.support:preference-v7:26.1.0' + implementation 'com.android.support:preference-v14:26.1.0' + compile 'com.github.wendykierp:JTransforms:3.1' +} diff --git a/android/ConductorsSensor/app/proguard-rules.pro b/android/ConductorsSensor/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/ConductorsSensor/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/ConductorsSensor/app/src/androidTest/java/de/tonifetzer/conductorssensor/ExampleInstrumentedTest.java b/android/ConductorsSensor/app/src/androidTest/java/de/tonifetzer/conductorssensor/ExampleInstrumentedTest.java new file mode 100644 index 0000000..760d1bb --- /dev/null +++ b/android/ConductorsSensor/app/src/androidTest/java/de/tonifetzer/conductorssensor/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package de.tonifetzer.conductorssensor; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("de.tonifetzer.conductorssensor", appContext.getPackageName()); + } +} diff --git a/android/ConductorsSensor/app/src/main/AndroidManifest.xml b/android/ConductorsSensor/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..08c7f1b --- /dev/null +++ b/android/ConductorsSensor/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/MainActivity.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/MainActivity.java new file mode 100644 index 0000000..9b76dda --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/MainActivity.java @@ -0,0 +1,155 @@ +package de.tonifetzer.conductorssensor; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.os.Bundle; +import android.support.v7.widget.PopupMenu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.Toast; +import android.widget.ToggleButton; + +import java.util.Stack; + +import de.tonifetzer.conductorssensor.estimation.Estimator; +import de.tonifetzer.conductorssensor.sensor.ConnectFragment; +import de.tonifetzer.conductorssensor.settings.SettingsFragment; + +public class MainActivity extends FragmentActivity implements PopupMenu.OnMenuItemClickListener, ToggleButton.OnCheckedChangeListener{ + + ConnectFragment mConnectFragment; + Stack mFragmentStack; + ToggleButton mStartStopToggle; + + Estimator mEstimator; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // register listener for start stop btn + mStartStopToggle= (ToggleButton) findViewById(R.id.startBtn); + mStartStopToggle.setOnCheckedChangeListener(this); + + // ensures the connection to the bt sensor board + mConnectFragment = new ConnectFragment(); + mFragmentStack = new Stack<>(); + } + + @Override + public void onBackPressed() { + + FragmentTransaction t = getSupportFragmentManager().beginTransaction(); + if(!mFragmentStack.empty()){ + // hide current top, and pop it + String top = mFragmentStack.pop(); + t.hide(getSupportFragmentManager().findFragmentByTag(top)); + + if(!mFragmentStack.empty()){ + //open new top of stack + top = mFragmentStack.peek(); + t.show(getSupportFragmentManager().findFragmentByTag(top)); + } else{ + // if its now empty activate our mainView + findViewById(R.id.bpmContent).setVisibility(View.VISIBLE); + findViewById(R.id.backBtn).setVisibility(View.INVISIBLE); + } + + t.commit(); + + } else { + super.onBackPressed(); + } + } + + public void onBackPressed(View v){ + onBackPressed(); + } + + public void openFragment(Fragment aFragment, String tag){ + + FragmentTransaction t = getSupportFragmentManager().beginTransaction(); + + if(!mFragmentStack.empty()){ + if(mFragmentStack.peek().equals(tag)){ + // if fragment is already open, do nothing + return; + } else { + //hide to current top + String top = mFragmentStack.peek(); + t.hide(getSupportFragmentManager().findFragmentByTag(top)); + } + } else { + //disable mainView items + findViewById(R.id.bpmContent).setVisibility(View.INVISIBLE); + findViewById(R.id.backBtn).setVisibility(View.VISIBLE); + } + + if (getSupportFragmentManager().findFragmentByTag(tag) == null) { + t.add(R.id.mainContent, aFragment, tag); + } else { + t.show(getSupportFragmentManager().findFragmentByTag(tag)); + } + + mFragmentStack.push(tag); + t.commit(); + } + + public void showPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.settings, popup.getMenu()); + + popup.setOnMenuItemClickListener(this); + popup.show(); + + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + + switch (item.getItemId()) { + case R.id.action_bluetooth: + openFragment(mConnectFragment, "Connect"); + break; + case R.id.action_settings: + openFragment(new SettingsFragment(), "Settings"); + break; + } + return false; + } + + @Override + public void finish() { + //super.finish(); + moveTaskToBack(true); + } + + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + + if (isChecked) { + if(mConnectFragment.getSensorBoard() != null && mConnectFragment.getSensorBoard().isConnected()){ + mConnectFragment.getSensorBoard().startAccelerometer(); + + //todo: estimator classe mit start und stop funktion. board bei start durchreichen. + + } else { + Toast.makeText(this, "Please connect a sensor!", Toast.LENGTH_SHORT).show(); + mStartStopToggle.setChecked(false); + } + + } else { + if(mConnectFragment.getSensorBoard() != null){ + mConnectFragment.getSensorBoard().stopAccelerometer(); + } + + } + + } +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerData.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerData.java new file mode 100644 index 0000000..14a96ed --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerData.java @@ -0,0 +1,35 @@ +package de.tonifetzer.conductorssensor.estimation; + +public class AccelerometerData { + + public double x,y,z; + public long ts; + + public AccelerometerData(long ts, double x, double y, double z){ + this.ts = ts; + this.x = x; + this.y = y; + this.z = z; + } + + public AccelerometerData(AccelerometerData other){ + this.ts = other.ts; + this.x = other.x; + this.y = other.y; + this.z = other.z; + } + + @Override + public boolean equals(Object other){ + + if (this == other) + return true; + + if (!(other instanceof AccelerometerData)) { + return false; + } + + AccelerometerData ad = (AccelerometerData) other; + return ts == ad.ts; + } +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerInterpolator.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerInterpolator.java new file mode 100644 index 0000000..dc1c533 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerInterpolator.java @@ -0,0 +1,105 @@ +package de.tonifetzer.conductorssensor.estimation; + +import java.util.Arrays; + +public class AccelerometerInterpolator { + + private double[] mX; + private double[] mY; + private double[] mZ; + private long[] mTsInterp; + + public AccelerometerInterpolator(AccelerometerWindowBuffer ab, double sampleRate_ms){ + + long size = (ab.getYongest().ts - (ab.getOldest().ts - (long) sampleRate_ms)) / (long) sampleRate_ms; + mTsInterp = new long[(int)size]; + int j = 0; + for(long i = ab.getOldest().ts; i <= ab.getYongest().ts; i += sampleRate_ms){ + mTsInterp[j++] = i; + } + + mX = interpLinear(ab.getTs(), ab.getX(), mTsInterp); + mY = interpLinear(ab.getTs(), ab.getY(), mTsInterp); + mZ = interpLinear(ab.getTs(), ab.getZ(), mTsInterp); + } + + public long[] getTs(){ + return mTsInterp; + } + + public double[] getX(){ + return mX; + } + + public double[] getY(){ + return mY; + } + + public double[] getZ(){ + return mZ; + } + + private static double[] interpLinear(double[] x, double[] y, double[] xi) throws IllegalArgumentException { + if (x.length != y.length) { + throw new IllegalArgumentException("X and Y must be the same length"); + } + if (x.length == 1) { + throw new IllegalArgumentException("X must contain more than one value"); + } + double[] dx = new double[x.length - 1]; + double[] dy = new double[x.length - 1]; + double[] slope = new double[x.length - 1]; + double[] intercept = new double[x.length - 1]; + + // Calculate the line equation (i.e. slope and intercept) between each point + for (int i = 0; i < x.length - 1; i++) { + dx[i] = x[i + 1] - x[i]; + if (dx[i] == 0) { + throw new IllegalArgumentException("X must be montotonic. A duplicate " + "x-value was found"); + } + if (dx[i] < 0) { + throw new IllegalArgumentException("X must be sorted"); + } + dy[i] = y[i + 1] - y[i]; + slope[i] = dy[i] / dx[i]; + intercept[i] = y[i] - x[i] * slope[i]; + } + + // Perform the interpolation here + double[] yi = new double[xi.length]; + for (int i = 0; i < xi.length; i++) { + if ((xi[i] > x[x.length - 1]) || (xi[i] < x[0])) { + yi[i] = Double.NaN; + } + else { + int loc = Arrays.binarySearch(x, xi[i]); + if (loc < -1) { + loc = -loc - 2; + yi[i] = slope[loc] * xi[i] + intercept[loc]; + } + else { + yi[i] = y[loc]; + } + } + } + + return yi; + } + + private static double[] interpLinear(long[] x, double[] y, long[] xi) throws IllegalArgumentException { + + double[] xd = new double[x.length]; + for (int i = 0; i < x.length; i++) { + xd[i] = x[i]; + } + + double[] xid = new double[xi.length]; + for (int i = 0; i < xi.length; i++) { + xid[i] = xi[i]; + } + + return interpLinear(xd, y, xid); + } + + +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerWindowBuffer.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerWindowBuffer.java new file mode 100644 index 0000000..4380b99 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AccelerometerWindowBuffer.java @@ -0,0 +1,148 @@ +package de.tonifetzer.conductorssensor.estimation; + +import java.util.ArrayList; + +/** + * Created by toni on 15/12/17. + */ +public class AccelerometerWindowBuffer extends ArrayList { + + private int mWindowSize; + private int mOverlapSize; + private int mOverlapCounter; + + public AccelerometerWindowBuffer(int windowSize_ms, int overlap_ms) { + mWindowSize = windowSize_ms; + mOverlapSize = overlap_ms; + mOverlapCounter = 0; + } + + //TODO: add exception handling. falseArgument if ad has no numeric x,y,z + public boolean add(AccelerometerData ad) { + synchronized (this) { + + //do not add duplicates! + if (!isEmpty() && getYongest().equals(ad)) { + return false; + } + + // current - last to increment overlap time + if (!isEmpty()) { + mOverlapCounter += ad.ts - getYongest().ts; + } + + //add element + boolean r = super.add(ad); + removeOldElements(); + + return r; + } + } + + private void removeOldElements() { + synchronized (this) { + if (!isEmpty()) { + if ((getYongest().ts - getOldest().ts) > mWindowSize) { + + long oldestTime = getYongest().ts - mWindowSize; + for (int i = 0; i < size(); ++i) { + if (get(i).ts > oldestTime) { + break; + } + super.remove(i); + } + } + } + } + } + + public boolean isNextWindowReady(long lastWindowTS, int overlapSize) { + return true; + } + + public boolean isNextWindowReady() { + + if (!isEmpty()) { + if (((getYongest().ts - getOldest().ts) > mWindowSize / 4) && mOverlapCounter > mOverlapSize) { + mOverlapCounter = 0; + + return true; + } + } + return false; + } + + public AccelerometerWindowBuffer getFixedSizedWindow(int size, int overlap) { + AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(size, overlap); + + double sampleRate = ((getYongest().ts - getOldest().ts) / super.size()); + + //if current size is smaller then wanted size, start at 0 and provide smaller list + int start = 0; + if ((getYongest().ts - getOldest().ts) > size) { + start = (int) Math.round(super.size() - (size / sampleRate)); + } + + // start should not be negative, this can happen due to rounding errors. + start = start < 0 ? 0 : start; + + synchronized (this) { + other.addAll(super.subList(start, super.size())); + } + + return other; + } + + public AccelerometerWindowBuffer getFixedSizedWindow() { + return getFixedSizedWindow(mWindowSize, mOverlapSize); + } + + //TODO: check if list is empty! this causes indexoutofbounce + public AccelerometerData getYongest() { + synchronized (this) { + return super.get(size() - 1); + } + } + + public AccelerometerData getOldest() { + return super.get(0); + } + + public double[] getX() { + return this.stream().mapToDouble(d -> d.x).toArray(); + } + + public double[] getY() { + return this.stream().mapToDouble(d -> d.y).toArray(); + } + + public double[] getZ() { + return this.stream().mapToDouble(d -> d.z).toArray(); + } + + public long[] getTs() { + return this.stream().mapToLong(d -> d.ts).toArray(); + } + + public int getOverlapSize() { + return mOverlapSize; + } + + public void setWindowSize(int size_ms) { + this.mWindowSize = size_ms; + removeOldElements(); // need to call this here, to remove too old elements, if the windowSize gets smaller. + } + + public void setOverlapSize(int size_ms) { + this.mOverlapSize = size_ms; + this.mOverlapCounter = 0; + } + + @Override + public void clear() { + synchronized (this) { + super.clear(); + this.mOverlapCounter = 0; + } + } +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AutoCorrelation.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AutoCorrelation.java new file mode 100644 index 0000000..be39db4 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/AutoCorrelation.java @@ -0,0 +1,80 @@ +package de.tonifetzer.conductorssensor.estimation; + + +import org.jtransforms.fft.DoubleFFT_1D; +import de.tonifetzer.conductorssensor.utilities.Utils; +import java.util.Arrays; + +/** + * Created by toni on 15/12/17. + */ +public class AutoCorrelation { + + private int mMaxLag; + private double[] mCorr; + + public AutoCorrelation(double[] data, int maxLag){ + + mMaxLag = maxLag; + mCorr = fft(data); + } + + public double[] getCorr(){ + return mCorr; + } + + private double[] fft(double[] data) { + + if(mMaxLag < 1){ + throw new RuntimeException("maxlag has to be greater 1"); + } + + int n = data.length; + double[] x = Arrays.copyOf(data, n); + int mxl = Math.min(mMaxLag, n - 1); + int ceilLog2 = Utils.nextPow2(2*n -1); + int n2 = (int) Math.pow(2,ceilLog2); + + // x - mean(x) (pointwise) + double x_mean = Utils.mean(x); + for(int i = 0; i < x.length; ++i){ + x[i] -= x_mean; + } + + // double the size of x and fill up with zeros. if x is not even, add additional 0 + double[] x2 = new double[n2 * 2]; //need double the size for fft.realForwardFull (look into method description) + Arrays.fill(x2, 0); + System.arraycopy(x,0, x2, 0, x.length); + + // x_fft calculate fft 1D + DoubleFFT_1D fft = new DoubleFFT_1D(n2); + fft.realForwardFull(x2); + + // Cr = abs(x_fft).^2 (absolute with complex numbers is (r^2) + (i^2) + double[] Cr = new double[n2 * 2]; + int j = 0; + for(int i = 0; i < x2.length; ++i){ + Cr[j++] = Utils.sqr(x2[i]) + Utils.sqr(x2[i+1]); + ++i; //skip the complex part + } + + // ifft(Cr,[],1) + DoubleFFT_1D ifft = new DoubleFFT_1D(n2); + ifft.realInverseFull(Cr, true); + + // remove complex part and scale/normalize + double[] c1 = new double[n2]; + j = 0; + for(int i = 0; i < Cr.length; ++i){ + c1[j++] = Cr[i] / Cr[0]; + ++i; //skip the complex part + } + + // Keep only the lags we want and move negative lags before positive lags. + double[] c = new double[(mxl * 2) + 1]; + System.arraycopy(c1, 0, c, mxl, mxl + 1); // +1 to place the 1.0 in the middle of correlation + System.arraycopy(c1, n2 - mxl, c, 0, mxl); + + return c; + } +} \ No newline at end of file diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/BpmEstimator.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/BpmEstimator.java new file mode 100644 index 0000000..55cb641 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/BpmEstimator.java @@ -0,0 +1,213 @@ +package de.tonifetzer.conductorssensor.estimation; + +import de.tonifetzer.conductorssensor.utilities.MovingFilter; +import de.tonifetzer.conductorssensor.utilities.SimpleKalman; +import de.tonifetzer.conductorssensor.utilities.Utils; + +import java.util.LinkedList; + + +/** + * Created by toni on 17/12/17. + */ +public class BpmEstimator { + + private AccelerometerWindowBuffer mBuffer; + private double mSampleRate_ms; + + private LinkedList mBpmHistory_X; + private LinkedList mBpmHistory_Y; + private LinkedList mBpmHistory_Z; + + private LinkedList mBpmHistory; + private int mResetCounter; + private int mResetLimit_ms; + + private MovingFilter mMvg; + //private SimpleKalman mKalman; + + + public BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){ + mBuffer = windowBuffer; + mSampleRate_ms = sampleRate_ms; + + mBpmHistory_X = new LinkedList<>(); + mBpmHistory_Y = new LinkedList<>(); + mBpmHistory_Z = new LinkedList<>(); + + mBpmHistory = new LinkedList<>(); + mResetCounter = 0; + mResetLimit_ms = resetAfter_ms; + + + //TODO: this is to easy. since the new dyn. windowsize produces smaller update times, we need to consider something, that + //TODO: holds more values, if they are similar, an resets the history if not. + mMvg = new MovingFilter(2); + //mKalman = new SimpleKalman(); + } + + //TODO: we use the buffer from outside.. this buffer is continuously updated.. not good! + public double estimate(AccelerometerWindowBuffer fixedWindow){ + + double sampleRate = mSampleRate_ms; + if(sampleRate <= 0){ + sampleRate = Math.round(Utils.mean(Utils.diff(fixedWindow.getTs()))); + } + + if(sampleRate == 0){ + int breakhere = 0; + } + + AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate); + + //are we conducting? + //just look at the newest 512 samples + //List subBuffer = mBuffer.subList(mBuffer.size() - 512, mBuffer.size()); + + double[] xAutoCorr = new AutoCorrelation(interp.getX(), fixedWindow.size()).getCorr(); + double[] yAutoCorr = new AutoCorrelation(interp.getY(), fixedWindow.size()).getCorr(); + double[] zAutoCorr = new AutoCorrelation(interp.getZ(), fixedWindow.size()).getCorr(); + + + //find a peak within range of 250 ms + int peakWidth = (int) Math.round(250 / sampleRate); + Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false); + Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false); + Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false); + + mBpmHistory_X.add(pX.getBPM(sampleRate)); + mBpmHistory_Y.add(pY.getBPM(sampleRate)); + mBpmHistory_Z.add(pZ.getBPM(sampleRate)); + + double estimatedBPM = getBestBpmEstimation(pX, pY, pZ); + if(estimatedBPM != -1){ + + //moving avg (lohnt dann, wenn wir viele daten haben) + mMvg.add(estimatedBPM); + mBpmHistory.add(mMvg.getAverage()); + + //mBpmHistory.add(estimatedBPM); + + //moving median (lohnt nur bei konstantem tempo, da wir nur ein tempo damit gut halten können.) + //mMvg.add(estimatedBPM); + //mBpmHistory.add(mMvg.getMedian()); + + //kalman filter (lohnt dann, wenn wir konstantes tempo haben, mit startangabe!) + + //standard last element + //mBpmHistory.add(estimatedBPM); + //mResetCounter = 0; + } + else { + int resetAfter = (int) Math.round(mResetLimit_ms / (mBuffer.getOverlapSize())); + if(++mResetCounter > resetAfter){ + mBpmHistory.clear(); + + //TODO: send signal to clear. + //mBuffer.clear(); + mMvg.clear(); + mResetCounter = 0; + } + return -1; + } + + //last element + return mBpmHistory.getLast(); + } + + public double getMeanBpm(){ + return Utils.mean(mBpmHistory); + } + + public double getMedianBPM(){ + return Utils.median(mBpmHistory); + } + + private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException { + + int cntNumAxis = 0; + double sumCorr = 0; //to prevent division by zero + double sumRms = 0; + double sumNumInter = 0; + + double corrMeanX = 0, corrRmsX = 0; + int corrNumInterX = 0; + if(peaksX.hasPeaks()){ + corrMeanX = Utils.geometricMean(peaksX.getPeaksValueWithoutZeroIdxAndNegativeValues()); + corrRmsX = Utils.rms(peaksX.getPeaksValueWithoutZeroIdx()); + corrNumInterX = Utils.intersectionNumber(peaksX.getData(), 0.2f); + + ++cntNumAxis; + sumCorr += corrMeanX; + sumRms += corrRmsX; + sumNumInter += corrNumInterX; + } + + double corrMeanY = 0, corrRmsY = 0; + int corrNumInterY = 0; + if(peaksY.hasPeaks()){ + corrMeanY = Utils.geometricMean(peaksY.getPeaksValueWithoutZeroIdxAndNegativeValues()); + corrRmsY = Utils.rms(peaksY.getPeaksValueWithoutZeroIdx()); + corrNumInterY = Utils.intersectionNumber(peaksY.getData(), 0.2f); + + ++cntNumAxis; + sumCorr += corrMeanY; + sumRms += corrRmsY; + sumNumInter += corrNumInterY; + } + + double corrMeanZ = 0, corrRmsZ = 0; + int corrNumInterZ = 0; + if(peaksZ.hasPeaks()){ + corrMeanZ = Utils.geometricMean(peaksZ.getPeaksValueWithoutZeroIdxAndNegativeValues()); + corrRmsZ = Utils.rms(peaksZ.getPeaksValueWithoutZeroIdx()); + corrNumInterZ = Utils.intersectionNumber(peaksZ.getData(), 0.2f); + + ++cntNumAxis; + sumCorr += corrMeanZ; + sumRms += corrRmsZ; + sumNumInter += corrNumInterZ; + } + + //no peaks, reject + if(cntNumAxis == 0){ + //throw new IllegalArgumentException("All Peaks are empty! -> Reject Estimation"); + return -1; + } + + /* + System.out.println("RMS-X: " + corrRmsX); + System.out.println("GEO-X: " + corrMeanX); + System.out.println("INTER-X: " + corrNumInterX); + + System.out.println("RMS-Y: " + corrRmsY); + System.out.println("GEO-Y: " + corrMeanY); + System.out.println("INTER-Y: " + corrNumInterY); + + System.out.println("RMS-Z: " + corrRmsZ); + System.out.println("GEO-Z: " + corrMeanZ); + System.out.println("INTER-Z: " + corrNumInterZ); + */ + + //values to low, reject + //TODO: this is a pretty simple assumption. first shot! + if(corrRmsX < 0.25 && corrRmsY < 0.25 && corrRmsZ < 0.25){ + return -1; + } + + double quantityX = ((corrMeanX / sumCorr) + (corrRmsX / sumRms) + (corrNumInterX / sumNumInter)) / cntNumAxis; + double quantityY = ((corrMeanY / sumCorr) + (corrRmsY / sumRms) + (corrNumInterY / sumNumInter)) / cntNumAxis; + double quantityZ = ((corrMeanZ / sumCorr) + (corrRmsZ / sumRms) + (corrNumInterZ / sumNumInter)) / cntNumAxis; + + //get best axis by quantity and estimate bpm + if(quantityX > quantityY && quantityX > quantityZ){ + return mBpmHistory_X.getLast(); + } + else if(quantityY > quantityZ){ + return mBpmHistory_Y.getLast(); + } + else { + return mBpmHistory_Z.getLast(); + } + } +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/Estimator.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/Estimator.java new file mode 100644 index 0000000..c4d80f1 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/Estimator.java @@ -0,0 +1,166 @@ +package de.tonifetzer.conductorssensor.estimation; + +import android.hardware.Sensor; +import android.util.Log; + +import java.util.LinkedList; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.tonifetzer.conductorssensor.sensor.SensorBoard; +import de.tonifetzer.conductorssensor.utilities.Utils; + +public class Estimator implements SensorBoard.OnSensorBoardDataListener { + + private SensorBoard mSensorBoard; + private AccelerometerWindowBuffer mAccelerometerWindowBuffer; + private BpmEstimator mBpmEstimator; + + private Timer mTimer = new Timer(); + + Estimator(){ + + mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750); + mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000); + } + + public void start(SensorBoard sensorBoard){ + if(mSensorBoard != null){ + mSensorBoard = sensorBoard; + mSensorBoard.addListener(this); + mSensorBoard.startAccelerometer(); + + startWorker(); + + } else { + Log.i("Estimator","Cant start estimator. SensorBoard is null."); + } + } + + public void stop(){ + if(mSensorBoard != null){ + mSensorBoard.removeListener(this); + mSensorBoard.stopAccelerometer(); + + mAccelerometerWindowBuffer.clear(); + + } else { + Log.i("Estimator","Cant stop estimator. SensorBoard is null."); + } + } + + @Override + public void onAccelerometerChanged(AccelerometerData data) { + mAccelerometerWindowBuffer.add(data); + + //todo: save data into stream and write on disk + + } + + private void startWorker() { + + mTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + + if (mAccelerometerWindowBuffer.isNextWindowReady()) { + + LinkedList bpmList = new LinkedList<>(); + + //todo: wie viele dieser Klassen kann ich wegwerfen um das Ergebnis nich schlechter zu machen? + double bpm60 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow()); + double bpm85 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(3500, 750)); + double bpm110 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2600, 750)); + double bpm135 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2000, 750)); + double bpm160 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1600,750)); + double bpm200 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1200, 750)); + + //add to list + //todo: make this cool... + //vielleicht einen weighted mean? + bpmList.add(bpm60); + bpmList.add(bpm85); + bpmList.add(bpm110); //110, 135, 160 langen auch schon + bpmList.add(bpm135); + bpmList.add(bpm160); + bpmList.add(bpm200); + + //Log.d("BPM: ", bpmList.toString()); + + //remove all -1 and calc bpmMean + while(bpmList.remove(Double.valueOf(-1))) {} + + //remove outliers + //todo: aktuell wird die liste hier sortiert.. eig net so schön. + Utils.removeOutliersZScore(bpmList, 3.4); + //Utils.removeOutliersHeuristic(); + + //Log.d("BPM: ", bpmList.toString()); + + double bpm = -1; + if(!bpmList.isEmpty()) { + double bpmMean = Utils.mean(bpmList); + double bpmMedian = Utils.median(bpmList); + + double bpmDiffSlowFast = bpmList.getFirst() - bpmList.getLast(); + if (Math.abs(bpmDiffSlowFast) > 25) { + + double tmpBPM = bpmMean + 25; + + while(bpm == -1){ + if (tmpBPM < 60) { + bpm = bpm60; + if(bpm == -1){ + bpm = bpmMean; + } + } + else if (tmpBPM < 85) { + bpm = bpm85; + } else if (tmpBPM < 110) { + bpm = bpm110; + } else if (tmpBPM < 135) { + bpm = bpm135; + } else if (tmpBPM < 160) { + bpm = bpm160; + } else { + bpm = bpm200; + } + + tmpBPM -= 5; + } + + //Log.d("BPM: ", "CHANGE"); + + } else { + + bpm = bpmMean; + //Log.d("BPM: ", "STAY"); + } + + + } + + for (OnEstimationListener listener : mEstimationListeners) { + listener.onNewEstimationAvailable(bpm); + } + + } + } + }, 0, 100); + } + + + /** + * Interface for callback calculated bpm + */ + public interface OnEstimationListener { + void onNewEstimationAvailable(double bpm); + } + + private List mEstimationListeners = new CopyOnWriteArrayList<>(); + public void add(OnEstimationListener listener){mEstimationListeners.add(listener);} + public void remove(OnEstimationListener listener){mEstimationListeners.remove(listener);} + +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/Peaks.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/Peaks.java new file mode 100644 index 0000000..c9f3182 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/estimation/Peaks.java @@ -0,0 +1,206 @@ +package de.tonifetzer.conductorssensor.estimation; + +import de.tonifetzer.conductorssensor.utilities.Utils; + +import java.util.LinkedList; + +/** + * Created by toni on 15/12/17. + */ +public class Peaks { + + private LinkedList mPeaksIdx; //provide the idx within the given data array + private LinkedList mPeaksPos; //the real position within the data-rang e.g. lag -1024 to 1024 + private LinkedList mPeaksValue; //the value at mPeaksPos + + private double[] mData; + + /** + * Simple method for finding local maxima in an array + * @param data input + * @param width minimum distance between peaks + * @param threshold minimum value of peaks + * @param decayRate how quickly previous peaks are forgotten + * @param isRelative minimum value of peaks is relative to local average + * @return array of peaks + */ + public Peaks(double[] data, int width, double threshold, double decayRate, boolean isRelative){ + + this.mData = data; + this.mPeaksIdx = new LinkedList<>(); + this.mPeaksPos = new LinkedList<>(); + this.mPeaksValue = new LinkedList<>(); + + //create the peaks + simplePeakFinder(data, width, threshold, decayRate, isRelative); + + updateLists(); + } + + public LinkedList getPeaksIdx() { + return mPeaksIdx; + } + + public int[] getPeaksIdxAsArray() { + return mPeaksIdx.stream().mapToInt(i->i).toArray(); + } + + public LinkedList getPeaksPos() { + return mPeaksPos; + } + + public double[] getPeaksPosAsArray() { + return mPeaksPos.stream().mapToDouble(i -> i).toArray(); + } + + public LinkedList getPeaksValue() { + return mPeaksValue; + } + + public double[] getPeaksValueAsArray() { + return mPeaksValue.stream().mapToDouble(i -> i).toArray(); + } + + public double[] getData(){ + return mData; + } + + //TODO: implement findFalseDetectedPeaks + public void improveResults(double[] data){ + + updateLists(); + } + + public double[] getPeaksValueWithoutZeroIdx() { + + double[] values = new double[mPeaksIdx.size() - 1]; + int mid = (mData.length / 2); + int j = 0; + for(int i = 0; i < mPeaksIdx.size(); ++i){ + if(!(mPeaksIdx.get(i) == mid)){ + values[j] = mPeaksValue.get(i); + ++j; + } + } + return values; + } + + public double[] getPeaksValueWithoutNegativeValues() { + + double[] values = new double[mPeaksIdx.size() - 1]; + int i = 0; + for(Integer idx : mPeaksIdx) { + double curVal = mPeaksValue.get(idx); + if(curVal > 0){ + values[i] = curVal; + ++i; + } + } + + return values; + } + + public double[] getPeaksValueWithoutZeroIdxAndNegativeValues(){ + double[] values = new double[mPeaksIdx.size() - 1]; + int mid = (mData.length / 2); + int j = 0; + for(int i = 0; i < mPeaksIdx.size(); ++i){ + double curVal = mPeaksValue.get(i); + if(!(mPeaksIdx.get(i) == mid) && curVal > 0){ + values[j] = curVal; + ++j; + } + } + return values; + } + + public boolean hasPeaks() { + return mPeaksIdx.size() > 1; + } + + /** + * Provides an estimation of beats per minute given a samplerate in milliseconds + * @param sampleRate_ms + * @return bpm if peaks found and conducting activity recognized, else -1 + */ + public double getBPM(double sampleRate_ms){ + + //todo: rückweisungsklasse kann auch hier mit rein. + if(hasPeaks()){ + + //todo diff and mean method for linkedlists for speed + //return 60000 / Utils.mean(Utils.diff(mPeaksPos.stream().mapToDouble(i -> i * sampleRate_ms).toArray())); + return 60000 / (sampleRate_ms * Utils.mean(Utils.diff(mPeaksPos.stream().mapToDouble(i -> i).toArray()))); + } + + return -1; + } + + /** + * updates the position and values of the found peaks. + * call this if peaks are somewhat changed. + */ + private void updateLists(){ + //fill the position and the value lists + for(Integer idx : mPeaksIdx){ + int mid = (mData.length / 2); + mPeaksPos.add(idx - mid); + + mPeaksValue.add(mData[idx]); + } + } + + //TODO: findPeaks method identical to Matlab... with PeakProminence + + private void simplePeakFinder(double[] data, int width, double threshold, double decayRate, boolean isRelative) { + int maxp; + int mid = 0; + int end = data.length; + double av = data[0]; + while (mid < end) { + av = decayRate * av + (1 - decayRate) * data[mid]; + if (av < data[mid]) + av = data[mid]; + int i = mid - width; + if (i < 0) + i = 0; + int stop = mid + width + 1; + if (stop > data.length) + stop = data.length; + maxp = i; + for (i++; i < stop; i++) + if (data[i] > data[maxp]) + maxp = i; + if (maxp == mid) { + if (overThreshold(data, maxp, width, threshold, isRelative, av)){ + this.mPeaksIdx.add(new Integer(maxp)); + } + } + mid++; + } + } + + private boolean overThreshold(double[] data, int index, int width, + double threshold, boolean isRelative, + double av) { + int pre = 3; + int post = 1; + + if (data[index] < av) + return false; + if (isRelative) { + int iStart = index - pre * width; + if (iStart < 0) + iStart = 0; + int iStop = index + post * width; + if (iStop > data.length) + iStop = data.length; + double sum = 0; + int count = iStop - iStart; + while (iStart < iStop) + sum += data[iStart++]; + return (data[index] > sum / count + threshold); + } else + return (data[index] > threshold); + } +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/ConnectFragment.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/ConnectFragment.java new file mode 100644 index 0000000..b70527f --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/ConnectFragment.java @@ -0,0 +1,349 @@ +package de.tonifetzer.conductorssensor.sensor; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import de.tonifetzer.conductorssensor.R; + +//TODO: wenn gerät bereits gekoppelt und out of range. disconnected er oft nicht und dann kann ich selbst über einen scan das devices +//TODO: nicht mehr finden. kein plan woran das liegt. + + +public class ConnectFragment extends Fragment implements View.OnClickListener, AdapterView.OnItemClickListener, SensorBoard.OnSensorBoardConnectListener { + + private Button mRefreshButton; + private ProgressBar mProgressBar; + private ProgressBar mProgressBarBleItem; + + private ListView mListViewConnected; + private ArrayList mDeviceListConnectedAsStrings = new ArrayList<>(); + private ArrayAdapter mAdapterListViewConnected; + private SensorBoard mSensorBoard; + private boolean mConnectionPending = false; + + private ArrayList mDeviceListAsStrings = new ArrayList<>(); + private ListView mListViewNotConnected; + private ArrayAdapter mAdapterListViewNotConnected; + private Map mDeviceList = new LinkedHashMap<>();; + + private BluetoothAdapter mBluetoothAdapter; + private Handler mHandler; + private BluetoothLeScanner mLEScanner; + private ScanSettings settings; + private List filters; + + private static final long SCAN_PERIOD = 5000; + private static final int REQUEST_ENABLE_BT = 666; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View myView = inflater.inflate(R.layout.fragment_connect, container, false); + mListViewNotConnected = (ListView) myView.findViewById(R.id.listbte); + mListViewConnected = (ListView) myView.findViewById(R.id.listbteConnected); + mRefreshButton = (Button) myView.findViewById(R.id.btnRefresh); + mProgressBar = (ProgressBar) myView.findViewById(R.id.progressBar); + + //TODO: parentclass for the stuff below... this is fckn. ugly + //stuff for updating the listView of ble devices not connected + mAdapterListViewNotConnected = new ArrayAdapter<>(getContext(),R.layout.listview_costume, R.id.TextViewBleDevice, mDeviceListAsStrings); + mListViewNotConnected.setAdapter(mAdapterListViewNotConnected); + TextView tmpTxtView1 = new TextView(getContext()); + tmpTxtView1.setText("not connected"); + mListViewNotConnected.addHeaderView(tmpTxtView1); + + //stuff for updating the listView of ble devices already connected + mAdapterListViewConnected = new ArrayAdapter<>(getContext(),R.layout.listview_costume, R.id.TextViewBleDevice, mDeviceListConnectedAsStrings); + mListViewConnected.setAdapter(mAdapterListViewConnected); + TextView tmpTxtView2 = new TextView(getContext()); + tmpTxtView2.setText("connected"); + mListViewConnected.addHeaderView(tmpTxtView2); + + //set the click listener + mListViewNotConnected.setOnItemClickListener(this); + mListViewConnected.setOnItemClickListener(this); + + mRefreshButton.setOnClickListener(this); + + // Inflate the layout for this fragment + return myView; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + + // Use this check to determine whether BLE is supported on the device. Then + // you can selectively disable BLE-related features. + if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { + Toast.makeText(getActivity(), R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); + mRefreshButton.setText(R.string.ble_not_supported); + mRefreshButton.setEnabled(false); + getActivity().finish(); + } else { + //start bluetooth + final BluetoothManager bluetoothManager = (BluetoothManager) getActivity().getSystemService(Context.BLUETOOTH_SERVICE); + mBluetoothAdapter = bluetoothManager.getAdapter(); + } + + // init + mHandler = new Handler(); + mSensorBoard = new SensorBoard(getContext()); + mSensorBoard.addListener(this); + + super.onCreate(savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + + //TODO: automatically connect to device saved earlier + //TODO: implement this + //mUserChosenDevice = Settings::DefaultDevice.. oder sowas + updateListOfConnectedDevices(); + + if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); + } else { + if (Build.VERSION.SDK_INT >= 21) { + mLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); + settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build(); + filters = new ArrayList(); + } + scanLeDevice(true); + } + + } + + @Override + public void onPause() { + super.onPause(); + if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) { + scanLeDevice(false); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mSensorBoard.disconnect(); + mSensorBoard.onDestroy(); + } + + //todo: show rssi + private void scanLeDevice(final boolean enable) { + if (enable) { + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (Build.VERSION.SDK_INT < 21) { + mBluetoothAdapter.stopLeScan(mLeScanCallback); + } else { + mLEScanner.stopScan(mScanCallback); + } + + mRefreshButton.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + } + }, SCAN_PERIOD); + if (Build.VERSION.SDK_INT < 21) { + mBluetoothAdapter.startLeScan(mLeScanCallback); + } else { + mLEScanner.startScan(filters, settings, mScanCallback); + } + + //start btn spinner + mRefreshButton.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.VISIBLE); + + //clear the device lists + mDeviceList.clear(); + mDeviceListAsStrings.clear(); + mAdapterListViewNotConnected.notifyDataSetChanged(); + + } else { + if (Build.VERSION.SDK_INT < 21) { + mBluetoothAdapter.stopLeScan(mLeScanCallback); + } else { + mLEScanner.stopScan(mScanCallback); + } + + mRefreshButton.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + } + } + + private ScanCallback mScanCallback = new ScanCallback() { + + @Override + public void onScanResult(int callbackType, ScanResult result) { + Log.i("callbackType", String.valueOf(callbackType)); + Log.i("result", result.toString()); + + BluetoothDevice btDevice = result.getDevice(); + if(!mDeviceList.containsKey(btDevice.getAddress())){ + + mDeviceList.put(btDevice.getAddress(), btDevice); + mDeviceListAsStrings.add(btDevice.getName() + "\n" + btDevice.getAddress()); + mAdapterListViewNotConnected.notifyDataSetChanged(); + } + } + + @Override + public void onBatchScanResults(List results) { + for (ScanResult sr : results) { + Log.i("ScanResult - Results", sr.toString()); + } + } + + @Override + public void onScanFailed(int errorCode) { + Log.e("Scan Failed", "Error Code: " + errorCode); + } + }; + + private BluetoothAdapter.LeScanCallback mLeScanCallback = + new BluetoothAdapter.LeScanCallback() { + @Override + public void onLeScan(final BluetoothDevice device, int rssi, + byte[] scanRecord) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + Log.i("onLeScan", device.toString()); + + if(!mDeviceList.containsKey(device.getAddress())){ + + mDeviceList.put(device.getAddress(), device); + mDeviceListAsStrings.add(device.getName() + "\n" + device.getAddress()); + mAdapterListViewNotConnected.notifyDataSetChanged(); + } + } + }); + } + }; + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.btnRefresh: + scanLeDevice(true); + break; + } + } + + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + + //if someone clicks on the header, just return + if(id == -1 || mConnectionPending){ + return; + } + + //stop scan + scanLeDevice(false); + + if (mSensorBoard.isConnected()){ + + // if we clicked on the same device as connected, just disconnect and refresh unconnected list + if(adapterView.getId() == R.id.listbteConnected){ + mSensorBoard.disconnect(); + scanLeDevice(true); + return; + } + + mSensorBoard.disconnect(); + } + + if (!mDeviceList.isEmpty()){ + + mProgressBarBleItem = view.findViewById(R.id.ProgressBarConnecting); + mProgressBarBleItem.setVisibility(View.VISIBLE); + mConnectionPending = true; + + // connect fresh sensor + BluetoothDevice curDevice = (new ArrayList<>(mDeviceList.values())).get((int) id); + mSensorBoard.connect(curDevice); + } + + } + + public void updateListOfConnectedDevices(){ + mDeviceListConnectedAsStrings.clear(); + if(mSensorBoard.isConnected()){ + mDeviceListConnectedAsStrings.add(mSensorBoard.getName() + "\n" + mSensorBoard.getAddress()); + } + mAdapterListViewConnected.notifyDataSetChanged(); + } + + public void removeElementOfListOfNotConnectedDevices(String key){ + mDeviceList.remove(key); + mDeviceListAsStrings.clear(); + for(Map.Entry entry : mDeviceList.entrySet()){ + mDeviceListAsStrings.add(entry.getValue().getName()+ "\n" + entry.getValue().getAddress()); + } + mAdapterListViewNotConnected.notifyDataSetChanged(); + } + + @Override + public void onSensorConnected() { + + removeElementOfListOfNotConnectedDevices(mSensorBoard.getAddress()); + updateListOfConnectedDevices(); + mProgressBarBleItem.setVisibility(View.GONE); + mConnectionPending = false; + } + + @Override + public void onSensorConnectionFailed() { + mProgressBarBleItem.setVisibility(View.GONE); + mConnectionPending = false; + } + + @Override + public void onSensorDisconnected() { + + updateListOfConnectedDevices(); + Toast.makeText(getActivity(), "Disconnected", Toast.LENGTH_LONG).show(); + mProgressBarBleItem.setVisibility(View.GONE); + mConnectionPending = false; + } + + public SensorBoard getSensorBoard() { + return mSensorBoard; + } +} + diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/DeviceListView.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/DeviceListView.java new file mode 100644 index 0000000..a572928 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/DeviceListView.java @@ -0,0 +1,4 @@ +package de.tonifetzer.conductorssensor.sensor; + +public class DeviceListView { +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/SensorBoard.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/SensorBoard.java new file mode 100644 index 0000000..c11d45b --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/sensor/SensorBoard.java @@ -0,0 +1,268 @@ +package de.tonifetzer.conductorssensor.sensor; + +import android.bluetooth.BluetoothDevice; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; + +import com.mbientlab.metawear.Data; +import com.mbientlab.metawear.MetaWearBoard; +import com.mbientlab.metawear.Route; +import com.mbientlab.metawear.Subscriber; +import com.mbientlab.metawear.android.BtleService; +import com.mbientlab.metawear.builder.RouteBuilder; +import com.mbientlab.metawear.builder.RouteComponent; +import com.mbientlab.metawear.data.Acceleration; +import com.mbientlab.metawear.module.Accelerometer; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import bolts.Continuation; +import bolts.Task; +import de.tonifetzer.conductorssensor.estimation.AccelerometerData; + +public class SensorBoard implements ServiceConnection { + + private BtleService.LocalBinder serviceBinder; + private MetaWearBoard mMetaBoard; + private BluetoothDevice mBluetoothDevice; + private Accelerometer mAccelerometer; + + private boolean mBoardConnected = false; + + private Context mContext; + + SensorBoard(Context context){ + mContext = context; + + // bind the btleService from MetaWear + mContext.bindService(new Intent(mContext, BtleService.class), + this, Context.BIND_AUTO_CREATE); + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + serviceBinder = (BtleService.LocalBinder) iBinder; + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + + } + + protected void disconnect(){ + + if(!mBoardConnected){ + return; + } + + mMetaBoard.tearDown(); + + boolean isCompleted = false; + try { + isCompleted = mMetaBoard.disconnectAsync().waitForCompletion(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if(!isCompleted){ + Toast.makeText(mContext, "Failed to disconnect. Try again.", Toast.LENGTH_SHORT).show(); + Log.i("SensorBoard", "Failed to disconnect Metaboard after 5 seconds."); + return; + } + + serviceBinder.removeMetaWearBoard(mBluetoothDevice); + + //note: it is important to call this in main loop! + for(OnSensorBoardConnectListener listener : mConnectListeners){ + listener.onSensorDisconnected(); + } + + + mBluetoothDevice = null; + mBoardConnected = false; + + Toast.makeText(mContext, "Disconnected", Toast.LENGTH_LONG).show(); + + } + + protected void onDestroy() { + + // Unbind the service when the activity is destroyed + mContext.unbindService(this); + } + + protected void connect(BluetoothDevice device){ + + //connect with metawear + mMetaBoard = serviceBinder.getMetaWearBoard(device); + + mMetaBoard.connectAsync().continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + if (task.isFaulted()) { + Log.i("ConnectFragment", "Failed to connect BLE"); + + //send toast to UiThread + new Handler(Looper.getMainLooper()).post(new Runnable() { + public void run() { + + for(OnSensorBoardConnectListener listener : mConnectListeners){ + listener.onSensorConnectionFailed(); + } + + //bar.setVisibility(View.GONE); + Toast.makeText(mContext, "Failed to connect", Toast.LENGTH_LONG).show(); + } + }); + } else { + Log.i("ConnectFragment", "Connected BLE"); + + //send toast to UiThread + new Handler(Looper.getMainLooper()).post(new Runnable() { + public void run() { + + mBoardConnected = true; + mBluetoothDevice = device; + + //note: it is important to call this in main loop! + for(OnSensorBoardConnectListener listener : mConnectListeners){ + listener.onSensorConnected(); + } + + //bar.setVisibility(View.GONE); + Toast.makeText(mContext, "Connected", Toast.LENGTH_LONG).show(); + } + }); + + + //this need to be set after the connection is established! beware of async tasks + //TODO: this seems to work poorly.. why?! + mMetaBoard.onUnexpectedDisconnect(new MetaWearBoard.UnexpectedDisconnectHandler() { + @Override + public void disconnected(int status) { + Log.i("ConnectFragment", "Unexpectedly lost connection: " + status); + + //send toast to UiThread + new Handler(Looper.getMainLooper()).post(new Runnable() { + public void run() { + + for(OnSensorBoardConnectListener listener : mConnectListeners){ + listener.onSensorConnectionFailed(); + } + + //bar.setVisibility(View.GONE); + Toast.makeText(mContext, "Connection lost!", Toast.LENGTH_SHORT).show(); + } + }); + } + }); + + } + return null; + } + }); + } + + public boolean isConnected() { + return mBoardConnected && mMetaBoard.isConnected(); + } + + public String getAddress(){ + + if(!mBoardConnected){ + return "unkown"; + } + + return mBluetoothDevice.getAddress(); + } + + public String getName(){ + + if(!mBoardConnected){ + return "unkown"; + } + + return mBluetoothDevice.getName(); + } + + public void startAccelerometer(){ + + if(mMetaBoard.isConnected()){ + mAccelerometer = mMetaBoard.getModule(Accelerometer.class); + + mAccelerometer.configure() + .odr(75f) // 75hz + .commit(); + + mAccelerometer.acceleration().addRouteAsync(new RouteBuilder() { + @Override + public void configure(RouteComponent source) { + source.stream(new Subscriber() { + @Override + public void apply(Data data, Object... env) { + Log.i("MainActivity", data.value(Acceleration.class).toString()); + + for(OnSensorBoardDataListener listener : mDataListeners){ + listener.onAccelerometerChanged(new AccelerometerData( + System.currentTimeMillis(), + data.value(Acceleration.class).x(), + data.value(Acceleration.class).y(), + data.value(Acceleration.class).z())); + } + } + }); + } + }).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + mAccelerometer.acceleration().start(); + mAccelerometer.start(); + return null; + } + }); + + } + + } + + public void stopAccelerometer(){ + + if(mMetaBoard.isConnected()){ + mAccelerometer.stop(); + } + + } + + + /** + * Interface for callback onConnectionInfos + */ + public interface OnSensorBoardConnectListener { + void onSensorConnected(); + void onSensorConnectionFailed(); + void onSensorDisconnected(); + } + + private List mConnectListeners = new CopyOnWriteArrayList<>(); + public void addListener(OnSensorBoardConnectListener listener){mConnectListeners.add(listener);} + public void removeListener(OnSensorBoardConnectListener listener){mConnectListeners.remove(listener);} + + public interface OnSensorBoardDataListener { + void onAccelerometerChanged(AccelerometerData data); + } + + private List mDataListeners = new CopyOnWriteArrayList<>(); + public void addListener(OnSensorBoardDataListener listener){mDataListeners.add(listener);} + public void removeListener(OnSensorBoardDataListener listener){mDataListeners.remove(listener);} +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/settings/SettingsFragment.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/settings/SettingsFragment.java new file mode 100644 index 0000000..816a632 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/settings/SettingsFragment.java @@ -0,0 +1,18 @@ +package de.tonifetzer.conductorssensor.settings; + +import android.os.Bundle; +import android.support.v7.preference.PreferenceFragmentCompat; +import de.tonifetzer.conductorssensor.R; + + +/** + * Taken from: https://medium.com/@JakobUlbrich/building-a-settings-screen-for-android-part-1-5959aa49337c + */ +public class SettingsFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + // Load the Preferences from the XML file + addPreferencesFromResource(R.xml.app_preference); + } + +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/MovingFilter.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/MovingFilter.java new file mode 100644 index 0000000..7a65290 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/MovingFilter.java @@ -0,0 +1,35 @@ +package de.tonifetzer.conductorssensor.utilities; + +import java.util.LinkedList; + +public class MovingFilter { + + private int mSize; + private double mTotal = 0d; + private LinkedList mSamples = new LinkedList<>(); + + public MovingFilter(int size) { + this.mSize = size; + } + + public void add(double x) { + mTotal += x; + mSamples.add(x); + if(mSamples.size() > mSize){ + mTotal -= mSamples.remove(); + } + } + + public double getAverage() { + return mTotal / mSamples.size(); + } + + public double getMedian() { + return Utils.median(mSamples); + } + + public void clear(){ + mSamples.clear(); + mTotal = 0d; + } +} \ No newline at end of file diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/SimpleKalman.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/SimpleKalman.java new file mode 100644 index 0000000..a2d72d4 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/SimpleKalman.java @@ -0,0 +1,30 @@ +package de.tonifetzer.conductorssensor.utilities; + +public class SimpleKalman { + + private double mSigmaUpdate; + private double mSigmaPrediction; + private double mMu; + private double mSigma; + + SimpleKalman(double initialMu, double initialSigma, double sigmaUpdate, double sigmaPrediction){ + mSigmaUpdate = sigmaUpdate; + mSigmaPrediction = sigmaPrediction; + mMu = initialMu; + mSigma = initialSigma; + } + + public double update(double data){ + + //prediction + mMu = mMu; + mSigma += mSigmaPrediction; + + //update + double k = mSigma / (mSigma + mSigmaUpdate); + mMu = mMu + k * (data - mMu); + mSigma = (1 - k) * mSigma; + + return mMu; + } +} diff --git a/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/Utils.java b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/Utils.java new file mode 100644 index 0000000..db4f8b1 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/java/de/tonifetzer/conductorssensor/utilities/Utils.java @@ -0,0 +1,203 @@ +package de.tonifetzer.conductorssensor.utilities; + +import android.content.Context; +import android.content.res.Resources; +import android.util.DisplayMetrics; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +//TODO: change from double to generic type +public class Utils { + public static double getDistance(double x1, double y1, double x2, double y2) { + return (double) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + } + + public static double sqr(double x) { + return x * x; + } + + public static int nextPow2(int a){ + return a == 0 ? 0 : 32 - Integer.numberOfLeadingZeros(a - 1); + } + + public static double sum(double[] data){ + double sum = 0; + for (int i = 0; i < data.length; i++) { + sum += data[i]; + } + return sum; + } + + public static long sum(long[] data){ + long sum = 0; + for (int i = 0; i < data.length; i++) { + sum += data[i]; + } + return sum; + } + + public static double sum (List data){ + double sum = 0; + for (int i = 0; i < data.size(); i++) { + sum += data.get(i).doubleValue(); + } + return sum; + } + + //TODO: Could be slow.. faster method? + public static double rms(double[] nums) { + double sum = 0.0f; + for (double num : nums) + sum += num * num; + return Math.sqrt(sum / nums.length); + } + + public static double[] diff(double[] data){ + double[] diff = new double[data.length - 1]; + int i=0; + for(int j = 1; j < data.length; ++j){ + diff[i] = data[j] - data[i]; + ++i; + } + return diff; + } + + public static long[] diff(long[] data){ + long[] diff = new long[data.length - 1]; + int i=0; + for(int j = 1; j < data.length; ++j){ + diff[i] = data[j] - data[i]; + ++i; + } + return diff; + } + + public static double mean(double[] data){ + return sum(data) / data.length; + } + + public static double mean(long[] data){ + return (double) sum(data) / (double) data.length; + } + + public static double mean(List data){ + return sum(data) / data.size(); + } + + public static double median(List data){ + data.sort(Comparator.naturalOrder()); + + double median; + if (data.size() % 2 == 0) + median = (data.get(data.size()/2) + data.get(data.size()/2 - 1))/2; + else + median = data.get(data.size()/2); + + return median; + } + + public static double mad(List data){ + + double median = median(data); + + List tmpList = new ArrayList<>(); + for(double value : data){ + tmpList.add(Math.abs(value - median)); + } + + return median(tmpList); + } + + //TODO: Could be slow.. faster method? + public static double geometricMean(double[] data) { + double sum = data[0]; + for (int i = 1; i < data.length; i++) { + sum *= data[i]; + } + return Math.pow(sum, 1.0 / data.length); + } + + public static int intersectionNumber(double[] signal, double border){ + int cnt = 0; + boolean isSmallerValue = false; + boolean isBiggerValue = false; + + for(double value : signal){ + if(value < border){ + if(isBiggerValue){ + cnt++; + } + + isSmallerValue = true; + isBiggerValue = false; + } + else { + if(isSmallerValue){ + cnt++; + } + + isSmallerValue = false; + isBiggerValue = true; + } + } + + return cnt; + } + + public static double[] removeZero(double[] array){ + int j = 0; + for( int i=0; i 0){ + array[j++] = array[i]; + } + } + double[] newArray = new double[j]; + System.arraycopy( array, 0, newArray, 0, j ); + + return newArray; + } + + public static float convertDpToPixel(float dp, Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + + public static float convertPixelsToDp(float px, Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + + public static void removeOutliersZScore(List data, double score) { + + if(!data.isEmpty()){ + double median = median(data); + double mad = mad(data); + + for(Iterator it = data.iterator(); it.hasNext(); ){ + + double M = Math.abs((0.6745 * (it.next() - median)) / mad); + if (M > score){ it.remove(); } + } + } + } +} \ No newline at end of file diff --git a/android/ConductorsSensor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/ConductorsSensor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/android/ConductorsSensor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/ConductorsSensor/app/src/main/res/drawable/ic_iconmonstr_menu_1.xml b/android/ConductorsSensor/app/src/main/res/drawable/ic_iconmonstr_menu_1.xml new file mode 100644 index 0000000..d64595f --- /dev/null +++ b/android/ConductorsSensor/app/src/main/res/drawable/ic_iconmonstr_menu_1.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/ConductorsSensor/app/src/main/res/drawable/ic_iconmonstr_undo_4.xml b/android/ConductorsSensor/app/src/main/res/drawable/ic_iconmonstr_undo_4.xml new file mode 100644 index 0000000..93c900d --- /dev/null +++ b/android/ConductorsSensor/app/src/main/res/drawable/ic_iconmonstr_undo_4.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/ConductorsSensor/app/src/main/res/drawable/ic_launcher_background.xml b/android/ConductorsSensor/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/ConductorsSensor/app/src/main/res/layout/activity_main.xml b/android/ConductorsSensor/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4945e73 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/ConductorsSensor/app/src/main/res/layout/fragment_connect.xml b/android/ConductorsSensor/app/src/main/res/layout/fragment_connect.xml new file mode 100644 index 0000000..210f146 --- /dev/null +++ b/android/ConductorsSensor/app/src/main/res/layout/fragment_connect.xml @@ -0,0 +1,43 @@ + + + + + + + + + +