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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/ConductorsSensor/app/src/main/res/layout/listview_costume.xml b/android/ConductorsSensor/app/src/main/res/layout/listview_costume.xml
new file mode 100644
index 0000000..472213b
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/layout/listview_costume.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/ConductorsSensor/app/src/main/res/menu/settings.xml b/android/ConductorsSensor/app/src/main/res/menu/settings.xml
new file mode 100644
index 0000000..ac9f323
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/menu/settings.xml
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/ConductorsSensor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/ConductorsSensor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/ConductorsSensor/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/ConductorsSensor/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/ConductorsSensor/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/ConductorsSensor/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/ConductorsSensor/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/ConductorsSensor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/ConductorsSensor/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/ConductorsSensor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/ConductorsSensor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/ConductorsSensor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/android/ConductorsSensor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android/ConductorsSensor/app/src/main/res/values/colors.xml b/android/ConductorsSensor/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/android/ConductorsSensor/app/src/main/res/values/strings.xml b/android/ConductorsSensor/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2210290
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+ ConductorsSensor
+ settings
+ connect
+ menu
+ Device not supported!
+
diff --git a/android/ConductorsSensor/app/src/main/res/values/styles.xml b/android/ConductorsSensor/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..f8ba449
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/android/ConductorsSensor/app/src/main/res/xml/app_preference.xml b/android/ConductorsSensor/app/src/main/res/xml/app_preference.xml
new file mode 100644
index 0000000..96945df
--- /dev/null
+++ b/android/ConductorsSensor/app/src/main/res/xml/app_preference.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/ConductorsSensor/app/src/test/java/de/tonifetzer/conductorssensor/ExampleUnitTest.java b/android/ConductorsSensor/app/src/test/java/de/tonifetzer/conductorssensor/ExampleUnitTest.java
new file mode 100644
index 0000000..e29b631
--- /dev/null
+++ b/android/ConductorsSensor/app/src/test/java/de/tonifetzer/conductorssensor/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package de.tonifetzer.conductorssensor;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/android/ConductorsSensor/build.gradle b/android/ConductorsSensor/build.gradle
new file mode 100644
index 0000000..2c13ee7
--- /dev/null
+++ b/android/ConductorsSensor/build.gradle
@@ -0,0 +1,31 @@
+ // Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.2'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ ivy {
+ url "https://mbientlab.com/releases/ivyrep"
+ layout "gradle"
+ }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/android/ConductorsSensor/gradle.properties b/android/ConductorsSensor/gradle.properties
new file mode 100644
index 0000000..743d692
--- /dev/null
+++ b/android/ConductorsSensor/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/android/ConductorsSensor/gradle/wrapper/gradle-wrapper.jar b/android/ConductorsSensor/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7a3265e
Binary files /dev/null and b/android/ConductorsSensor/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/ConductorsSensor/gradle/wrapper/gradle-wrapper.properties b/android/ConductorsSensor/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..f27ae15
--- /dev/null
+++ b/android/ConductorsSensor/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed May 02 14:45:42 CEST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/android/ConductorsSensor/gradlew b/android/ConductorsSensor/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/android/ConductorsSensor/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/android/ConductorsSensor/gradlew.bat b/android/ConductorsSensor/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/android/ConductorsSensor/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/ConductorsSensor/settings.gradle b/android/ConductorsSensor/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/android/ConductorsSensor/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/android/ConductorsWatch/app/build.gradle b/android/ConductorsWatch/app/build.gradle
index 51b4425..2494041 100644
--- a/android/ConductorsWatch/app/build.gradle
+++ b/android/ConductorsWatch/app/build.gradle
@@ -9,8 +9,8 @@ android {
minSdkVersion 23
targetSdkVersion 26
//sdk 2 | product version 3 | build num 2 | multi-apk 2
- versionCode 260130301
- versionName "0.1.3.2"
+ versionCode 260130401
+ versionName "0.1.3.3"
}
buildTypes {
release {
diff --git a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java
index e7de7ff..cd8ea70 100644
--- a/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java
+++ b/android/ConductorsWatch/app/src/main/java/de/tonifetzer/conductorswatch/bpmEstimation/AccelerometerWindowBuffer.java
@@ -1,8 +1,6 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
-import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.ArrayList;
-import java.util.List;
/**
* Created by toni on 15/12/17.