15 Commits

Author SHA1 Message Date
toni
9c187a3992 aufräumen und refactoren 2019-01-30 20:11:34 +01:00
toni
80e8fd499d added moved files 2019-01-30 16:27:56 +01:00
toni
691f3a684a small refactoring 2019-01-30 16:24:42 +01:00
toni
72a672a80b fixed metronom to be more accurate 2019-01-29 17:11:27 +01:00
toni
a438813233 refactored mainactivity and moved some files into ui package 2019-01-29 16:35:43 +01:00
toni
838242ba76 fixed a bug when watch is not connected to a phone, we closed the datastream even its wasn openend 2019-01-27 19:08:35 +01:00
toni
a9532f8129 updated gradle und stuff...
fixed threading bug in tapping on the huawei watch
2019-01-27 16:42:22 +01:00
toni
49042a0cfb added ground truth to java method
fixed some bugs
improved algo and results
2019-01-27 10:47:46 +01:00
toni
6bb8bb6b4f added distance based correlation function in matlab and java 2018-12-14 16:29:34 +01:00
toni
42e38bd929 added magnitude
refactored plotting
refactored some code
2018-07-23 20:32:48 +02:00
toni
b8f20ec8d9 made mSensor App ready for recording session at manfreds house 2018-07-20 19:14:38 +02:00
toni
2883dd8060 ref #28 it works. it just works. 2018-05-18 23:32:20 +02:00
toni
38c39a045c adding conductorssensor using the mbientlab metaboard r+ 2018-05-18 22:07:42 +02:00
toni
85ea37c14b ref #27 -new gradle and sdk version
- added faster bpm estimation by using multiple windows now
- implemented a simple z score based outlier detection
- some ugly heuristics to prefend to many -1 on the screen
NOTE: this is a very unstable version of the code, very protoype
2018-04-27 16:03:58 +02:00
toni
cad85f8593 puhh. this is a big commit
- change the complete file sending / receiving process between phone and watch, we now use channels instead of simple messages. this is recommentad by google, due to some changes in google play services.
- made some smaller changes in the ui, for file saving and saving of sensordata for evaluation purposes
- edited the manifest and gradle script for play store.
- made some change for a better performance for he huawei watch
2018-04-13 11:52:12 +02:00
109 changed files with 5498 additions and 756 deletions

6
android/ConductorsPhone/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

View File

@@ -11,8 +11,8 @@ android {
minSdkVersion 24
targetSdkVersion 26
//sdk 2 | product version 3 | build num 2 | multi-apk 2
versionCode 260120100
versionName "0.1.2"
versionCode 260130400
versionName "0.1.3.3"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -33,12 +33,12 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
implementation 'com.google.android.support:wearable:2.1.0'
implementation 'com.google.android.support:wearable:2.2.0'
implementation 'com.google.android.gms:play-services-wearable:11.8.0'
implementation 'com.android.support:percent:26.1.0'
implementation 'com.android.support:animated-vector-drawable:26.1.0'
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:support-v4:26.1.0'
implementation 'com.android.support:recyclerview-v7:26.1.0'
compileOnly 'com.google.android.wearable:wearable:2.1.0'
compileOnly 'com.google.android.wearable:wearable:2.2.0'
}

View File

@@ -3,13 +3,12 @@
package="de.tonifetzer.conductorswatch">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme">

View File

@@ -1,8 +1,6 @@
package de.tonifetzer.conductorswatch;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import java.util.TimerTask;
@@ -10,7 +8,6 @@ import java.util.TimerTask;
/**
* Created by toni on 20/12/17.
*/
public class Metronome extends TimerTask {
//private MediaPlayer mMediaPlayer;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.3.0'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -1,6 +1,6 @@
#Tue Dec 19 11:59:51 CET 2017
#Tue Jan 29 16:48:07 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

10
android/ConductorsSensor/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.externalNativeBuild

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,38 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "de.tonifetzer.conductorssensor"
minSdkVersion 24
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'
implementation 'com.github.wendykierp:JTransforms:3.1'
}

View File

@@ -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

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.tonifetzer.conductorssensor">
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name="com.mbientlab.metawear.android.BtleService" />
</application>
</manifest>

View File

@@ -0,0 +1,235 @@
package de.tonifetzer.conductorssensor;
import android.Manifest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
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.util.Log;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.TextView;
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, Estimator.OnEstimationListener{
private static final int PERMISSION_REQUEST_COARSE_LOCATION = 1;
ConnectFragment mConnectFragment;
Stack<String> mFragmentStack;
ToggleButton mStartStopToggle;
TextView mBpmTextView;
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);
//textview to show the bpm estimation
mBpmTextView = (TextView) findViewById(R.id.bpmText);
mEstimator = new Estimator(this);
mEstimator.addListener(this);
// ensures the connection to the bt sensor board
mConnectFragment = new ConnectFragment();
mFragmentStack = new Stack<>();
//check for ble permissions
checkForBlePermissions();
}
@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()){
mBpmTextView.setTextColor(Color.parseColor("#EE693F"));
Toast.makeText(this, "Start estimation", Toast.LENGTH_SHORT).show();
mEstimator.start(mConnectFragment.getSensorBoard());
//todo: metronom
} else {
Toast.makeText(this, "Please connect a sensor!", Toast.LENGTH_SHORT).show();
mStartStopToggle.setChecked(false);
}
} else {
if(mConnectFragment.getSensorBoard() != null){
mBpmTextView.setTextColor(Color.parseColor("#158b69"));
Toast.makeText(this, "Stop estimation", Toast.LENGTH_SHORT).show();
mEstimator.stop();
}
}
}
@Override
public void onNewEstimationAvailable(double bpm) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mBpmTextView.setText(String.valueOf(Math.round(bpm)));
}
});
}
private void checkForBlePermissions(){
if(this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("This app needs location access");
builder.setMessage("Please grant location access so this app can detect the Sensor over Bluetooth.");
builder.setPositiveButton(android.R.string.ok, null);
builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialogInterface) {
requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSION_REQUEST_COARSE_LOCATION);
}
});
builder.show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permission[], int[] grantResults){
switch(requestCode){
case PERMISSION_REQUEST_COARSE_LOCATION: {
if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d("MainActivity", "coarse location permissin granted");
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Functionality limited");
builder.setMessage("Without permission, this app is not able to connect to a sensor.");
builder.setPositiveButton(android.R.string.ok, null);
builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialogInterface) {
}
});
builder.show();
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,10 +1,7 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
package de.tonifetzer.conductorssensor.estimation;
import java.util.Arrays;
/**
* Created by toni on 16/12/17.
*/
public class AccelerometerInterpolator {
private double[] mX;

View File

@@ -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<AccelerometerData> {
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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,12 +1,11 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
package de.tonifetzer.conductorssensor.estimation;
import de.tonifetzer.conductorswatch.utilities.MovingFilter;
import de.tonifetzer.conductorswatch.utilities.SimpleKalman;
import de.tonifetzer.conductorswatch.utilities.Utils;
import de.tonifetzer.conductorssensor.utilities.MovingFilter;
import de.tonifetzer.conductorssensor.utilities.SimpleKalman;
import de.tonifetzer.conductorssensor.utilities.Utils;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Created by toni on 17/12/17.
@@ -28,7 +27,7 @@ public class BpmEstimator {
//private SimpleKalman mKalman;
public BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
mBuffer = windowBuffer;
mSampleRate_ms = sampleRate_ms;
@@ -40,10 +39,21 @@ public class BpmEstimator {
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();
}
public void reset(){
mResetCounter = 0;
mBpmHistory_X.clear();
mBpmHistory_Y.clear();
mBpmHistory_Z.clear();
mBpmHistory.clear();
}
//TODO: we use the buffer from outside.. this buffer is continuously updated.. not good!
public double estimate(AccelerometerWindowBuffer fixedWindow){
@@ -52,19 +62,26 @@ public class BpmEstimator {
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<AccelerometerData> subBuffer = mBuffer.subList(mBuffer.size() - 512, mBuffer.size());
double[] xAutoCorr = new AutoCorrelation(interp.getX(), 512).getCorr();
double[] yAutoCorr = new AutoCorrelation(interp.getY(), 512).getCorr();
double[] zAutoCorr = new AutoCorrelation(interp.getZ(), 512).getCorr();
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();
Peaks pX = new Peaks(xAutoCorr, 50, 0.1f, 0, false);
Peaks pY = new Peaks(yAutoCorr, 50, 0.1f, 0, false);
Peaks pZ = new Peaks(zAutoCorr, 50, 0.1f, 0, false);
//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));
@@ -77,6 +94,8 @@ public class BpmEstimator {
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());
@@ -88,12 +107,12 @@ public class BpmEstimator {
//mResetCounter = 0;
}
else {
int resetAfter = (int) Math.round(mResetLimit_ms / (mBuffer.getOverlapSize() * sampleRate));
int resetAfter = (int) Math.round(mResetLimit_ms / (mBuffer.getOverlapSize()));
if(++mResetCounter > resetAfter){
mBpmHistory.clear();
//TODO: send signal to clear.
mBuffer.clear();
//mBuffer.clear();
mMvg.clear();
mResetCounter = 0;
}
@@ -181,7 +200,7 @@ public class BpmEstimator {
//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;
return -1;
}
double quantityX = ((corrMeanX / sumCorr) + (corrRmsX / sumRms) + (corrNumInterX / sumNumInter)) / cntNumAxis;

View File

@@ -0,0 +1,181 @@
package de.tonifetzer.conductorssensor.estimation;
import android.content.Context;
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.SensorDataFileWriter;
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();
private SensorDataFileWriter mFileWriter;
private Context mContext;
public Estimator(Context context){
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000);
mContext = context;
}
public void start(SensorBoard sensorBoard){
if(sensorBoard != null){
mSensorBoard = sensorBoard;
mSensorBoard.addListener(this);
//mSensorBoard.startAccelerometer();
mSensorBoard.startLinearAccelerometer();
mFileWriter = new SensorDataFileWriter(mContext);
startWorker();
} else {
Log.i("Estimator","Cant start estimator. SensorBoard is null.");
}
}
public void stop(){
if(mSensorBoard != null){
mSensorBoard.removeListener(this);
//mSensorBoard.stopAccelerometer();
mSensorBoard.stopLinearAccelerometer();
mAccelerometerWindowBuffer.clear();
mBpmEstimator.reset();
mFileWriter.toDisk();
} else {
Log.i("Estimator","Cant stop estimator. SensorBoard is null.");
}
}
@Override
public void onAccelerometerChanged(AccelerometerData data) {
mAccelerometerWindowBuffer.add(data);
//Log.d("Acc: ", "x: " + data.x + " y: " + data.y + " z: " + data.z);
//todo: save data into stream and write on disk
mFileWriter.writeVector3D(data.ts, 2, data.x, data.y, data.z);
}
private void startWorker() {
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (mAccelerometerWindowBuffer.isNextWindowReady()) {
LinkedList<Double> 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<OnEstimationListener> mEstimationListeners = new CopyOnWriteArrayList<>();
public void addListener(OnEstimationListener listener){mEstimationListeners.add(listener);}
public void removeListener(OnEstimationListener listener){mEstimationListeners.remove(listener);}
}

View File

@@ -1,6 +1,6 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
package de.tonifetzer.conductorssensor.estimation;
import de.tonifetzer.conductorswatch.utilities.Utils;
import de.tonifetzer.conductorssensor.utilities.Utils;
import java.util.LinkedList;
@@ -181,8 +181,8 @@ public class Peaks {
}
private boolean overThreshold(double[] data, int index, int width,
double threshold, boolean isRelative,
double av) {
double threshold, boolean isRelative,
double av) {
int pre = 3;
int post = 1;

View File

@@ -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<String> mDeviceListConnectedAsStrings = new ArrayList<>();
private ArrayAdapter<String> mAdapterListViewConnected;
private SensorBoard mSensorBoard;
private boolean mConnectionPending = false;
private ArrayList<String> mDeviceListAsStrings = new ArrayList<>();
private ListView mListViewNotConnected;
private ArrayAdapter<String> mAdapterListViewNotConnected;
private Map<String, BluetoothDevice> mDeviceList = new LinkedHashMap<>();;
private BluetoothAdapter mBluetoothAdapter;
private Handler mHandler;
private BluetoothLeScanner mLEScanner;
private ScanSettings settings;
private List<ScanFilter> 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<ScanFilter>();
}
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<ScanResult> 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<String, BluetoothDevice> 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;
}
}

View File

@@ -0,0 +1,4 @@
package de.tonifetzer.conductorssensor.sensor;
public class DeviceListView {
}

View File

@@ -0,0 +1,315 @@
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.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 com.mbientlab.metawear.module.SensorFusionBosch;
import com.mbientlab.metawear.module.SensorFusionBosch.*;
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 SensorFusionBosch mSensorFusion;
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<Void, Void>() {
@Override
public Void then(Task<Void> 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
.range(4f)
.commit();
mAccelerometer.acceleration().addRouteAsync(new RouteBuilder() {
@Override
public void configure(RouteComponent source) {
source.stream(new Subscriber() {
@Override
public void apply(Data data, Object... env) {
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<Route, Void>() {
@Override
public Void then(Task<Route> task) throws Exception {
mAccelerometer.acceleration().start();
mAccelerometer.start();
return null;
}
});
}
}
public void stopAccelerometer(){
if(mMetaBoard.isConnected()){
mAccelerometer.stop();
}
}
public void startLinearAccelerometer(){
if(mMetaBoard.isConnected()){
mSensorFusion = mMetaBoard.getModule(SensorFusionBosch.class);
mSensorFusion.configure()
.mode(Mode.NDOF)
.accRange(AccRange.AR_16G)
.gyroRange(GyroRange.GR_2000DPS)
.commit();
mSensorFusion.linearAcceleration().addRouteAsync(new RouteBuilder() {
@Override
public void configure(RouteComponent source) {
source.stream(new Subscriber() {
@Override
public void apply(Data data, Object... env) {
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<Route, Void>() {
@Override
public Void then(Task<Route> task) throws Exception {
mSensorFusion.linearAcceleration().start();
mSensorFusion.start();
return null;
}
});
}
}
public void stopLinearAccelerometer(){
if(mMetaBoard.isConnected()){
mSensorFusion.stop();
}
}
/**
* Interface for callback onConnectionInfos
*/
public interface OnSensorBoardConnectListener {
void onSensorConnected();
void onSensorConnectionFailed();
void onSensorDisconnected();
}
private List<OnSensorBoardConnectListener> 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<OnSensorBoardDataListener> mDataListeners = new CopyOnWriteArrayList<>();
public void addListener(OnSensorBoardDataListener listener){mDataListeners.add(listener);}
public void removeListener(OnSensorBoardDataListener listener){mDataListeners.remove(listener);}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,58 @@
package de.tonifetzer.conductorssensor.utilities;
/**
* Created by toni on 12/01/18.
*/
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import java.io.File;
/**
* Class: DataFolder
* Description: SDK save file class. Is able to open a folder on the device independent of the given
* device and android skd version.
*/
public class DataFolder {
private File folder;
private static final String TAG = "DataFolder";
public DataFolder(Context context, String folderName){
// 1) try external data folder
folder = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), folderName);
if (isOK(folder)) {return;}
// 2) try sd-card folder
folder = new File(Environment.getExternalStorageDirectory() + "/" + folderName);
if (isOK(folder)) {return;}
// 3) try internal data folder
folder = new File(context.getApplicationInfo().dataDir);
if (isOK(folder)) {return;}
// all failed
throw new RuntimeException("failed to create/access storage folder");
}
/** ensure the given folder is OK */
private static final boolean isOK(final File folder) {
folder.mkdirs();
final boolean ok = folder.exists() && folder.isDirectory();
if (ok) {
Log.d(TAG, "using: " + folder);
} else {
Log.d(TAG, "not OK: " + folder);
}
return ok;
}
public File getFolder(){
return folder;
}
}

View File

@@ -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<Double> 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;
}
}

View File

@@ -0,0 +1,138 @@
package de.tonifetzer.conductorssensor.utilities;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.EditText;
import android.widget.TextView;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import de.tonifetzer.conductorssensor.R;
/**
* Created by toni on 14/01/18.
*/
public class SensorDataFileWriter {
private static final int FLUSH_LIMIT = 2*1024*1024;
private static final String TAG = "SensorDataFileWriter";
private File mSensorDataFile;
private FileOutputStream mFileOutputStream;
private StringBuilder mStringBuilder = new StringBuilder();
private final DataFolder mFolder;
private boolean mStreamOpenend = false;
public SensorDataFileWriter(Context context) {
mFolder = new DataFolder(context, "sensorOutFiles");
//write some description for this file in the first line
EditText editView = ((Activity) context).findViewById(R.id.comments);
TextView textView = ((Activity) context).findViewById(R.id.bpmText);
writeDescription("Kommentar: " + editView.getText().toString() + "\nMetronom: " + textView.getText().toString() + " bpm");
//open connection to file
open();
}
public final void writeVector3D(long ts, int id, double x, double y, double z){
synchronized (this) {
if (mStreamOpenend) {
mStringBuilder.append(ts).append(';').append(id).append(';').append(x).append(';').append(y).append(';').append(z).append('\n');
if (mStringBuilder.length() > FLUSH_LIMIT) {flush(false);}
} else {
open();
}
}
}
private void writeDescription(String str){
synchronized (this) {
mStringBuilder.append(str).append('\n').append('\n');
}
}
// flush data to disk
public final void toDisk(){
synchronized (this) {
flush(true);
close();
}
}
/** helper method for exception-less writing. DO NOT CALL DIRECTLY! */
private void _write(final byte[] data) {
try {
mFileOutputStream.write(data);
Log.d(TAG, "flushed " + data.length + " bytes to disk");
} catch (final Exception e) {
throw new RuntimeException("error while writing log-file", e);
}
}
/** helper-class for background writing */
class FlushAsync extends AsyncTask<byte[], Integer, Integer> {
@Override
protected final Integer doInBackground(byte[][] data) {
_write(data[0]);
return null;
}
};
/** flush current buffer-contents to disk */
private void flush(boolean sync) {
// fetch current buffer contents to write and hereafter empty the buffer
// this action MUST be atomic, just like the add-method
byte[] data = null;
synchronized (this) {
data = mStringBuilder.toString().getBytes(); // fetch data to write
mStringBuilder.setLength(0); // reset the buffer
}
// write
if (sync) {
// write to disk using the current thread
_write(data);
} else {
// write to disk using a background-thread
new FlushAsync().execute(new byte[][] {data});
}
}
private void open() {
mSensorDataFile = new File(mFolder.getFolder(), System.currentTimeMillis() + ".csv");
try {
mFileOutputStream = new FileOutputStream(mSensorDataFile);
mStreamOpenend = true;
Log.d(TAG, "will write to: " + mSensorDataFile.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
mStreamOpenend = false;
}
}
private void close() {
try {
mFileOutputStream.close();
mStreamOpenend = false;
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<Double> 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<Double> data){
return sum(data) / data.size();
}
public static double median(List<Double> 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<Double> data){
double median = median(data);
List<Double> 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<array.length; i++ )
{
if (array[i] != 0){
array[j++] = array[i];
}
}
double[] newArray = new double[j];
System.arraycopy( array, 0, newArray, 0, j );
return newArray;
}
public static double[] greaterZero(double[] array){
int j = 0;
for( int i=0; i < array.length; i++ )
{
if (array[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<Double> data, double score) {
if(!data.isEmpty()){
double median = median(data);
double mad = mad(data);
for(Iterator<Double> it = data.iterator(); it.hasNext(); ){
double M = Math.abs((0.6745 * (it.next() - median)) / mad);
if (M > score){ it.remove(); }
}
}
}
}

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M24,6h-24v-4h24v4zM24,10h-24v4h24v-4zM24,18h-24v4h24v-4z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M17.026,22.957c10.957,-11.421 -2.326,-20.865 -10.384,-13.309l2.464,2.352h-9.106v-8.947l2.232,2.229c14.794,-13.203 31.51,7.051 14.794,17.675z"/>
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/main">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/topbar"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/relativeLayout1"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:contentDescription="@string/descr_overflow_button"
android:onClick="onBackPressed"
android:src="@drawable/ic_iconmonstr_undo_4"
android:background="@null"
android:paddingLeft="12dp"
android:paddingTop="12dp"
android:visibility="invisible"
android:id="@+id/backBtn"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/descr_overflow_button"
android:onClick="showPopup"
android:src="@drawable/ic_iconmonstr_menu_1"
android:layout_alignParentEnd="true"
android:background="@null"
android:paddingRight="12dp"
android:paddingTop="12dp"/>
</RelativeLayout>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mainContent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:id="@+id/bpmContent">
<ToggleButton
android:id="@+id/startBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textOn="Recording"
android:textOff="Off" />
<TextView
android:id="@+id/bpmText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:soundEffectsEnabled="true"
android:text="80"
android:textColor="#158b69"
android:textSize="100sp"
android:textStyle="bold"
android:layout_centerHorizontal="true"/>
<TextView
android:id="@+id/Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:soundEffectsEnabled="true"
android:text="Kommentare zur Aufnahme:"
android:textStyle="bold" />
<EditText
android:id="@+id/comments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textMultiLine"
android:text=""
android:layout_centerHorizontal="true"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:id="@+id/settingsContainer">
<ListView android:id="@+id/listbteConnected"
android:layout_height="wrap_content"
android:layout_width="match_parent" />
<ListView android:id="@+id/listbte"
android:layout_height="0dip"
android:layout_width="match_parent"
android:layout_weight="1" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Refresh"
android:id="@+id/btnRefresh"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:padding="20dp"
android:focusable="false"
android:focusableInTouchMode="false" />
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_alignBottom="@+id/btnRefresh"
android:layout_marginBottom="3dp"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2006 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/TextViewBleDevice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:minHeight="?android:attr/listPreferredItemHeightSmall" />
<ProgressBar
android:id="@+id/ProgressBarConnecting"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="4dp"
android:visibility="gone"/>
<ImageView
android:id="@+id/imageViewConnected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/presence_online"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="4dp"
android:visibility="gone"
android:contentDescription="connected" />
</FrameLayout>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_bluetooth"
android:title="@string/connect_bluetooth"
app:showAsAction="never"/>
<!-- Settings, should always be in the overflow -->
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
app:showAsAction="never"/>
</menu>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@@ -0,0 +1,7 @@
<resources>
<string name="app_name">ConductorsSensor</string>
<string name="action_settings">settings</string>
<string name="connect_bluetooth">connect</string>
<string name="descr_overflow_button">menu</string>
<string name="ble_not_supported">Device not supported!</string>
</resources>

View File

@@ -0,0 +1,13 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
</resources>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/settingsContainer">
<android.support.v7.preference.PreferenceCategory
android:title="Category 1">
<android.support.v7.preference.SwitchPreferenceCompat
android:key="key1"
android:title="Switch Preference"
android:summary="Switch Summary"
android:defaultValue="true" />
<android.support.v7.preference.EditTextPreference
android:key="key2"
android:title="EditText Preference"
android:summary="EditText Summary"
android:dialogMessage="Dialog Message"
android:defaultValue="Default value" />
<android.support.v7.preference.CheckBoxPreference
android:key="key3"
android:title="CheckBox Preference"
android:summary="CheckBox Summary"
android:defaultValue="true"/>
</android.support.v7.preference.PreferenceCategory>
</android.support.v7.preference.PreferenceScreen>

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

@@ -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
}

View File

@@ -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

Binary file not shown.

View File

@@ -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

172
android/ConductorsSensor/gradlew vendored Executable file
View File

@@ -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" "$@"

84
android/ConductorsSensor/gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
include ':app'

View File

@@ -9,8 +9,8 @@ android {
minSdkVersion 23
targetSdkVersion 26
//sdk 2 | product version 3 | build num 2 | multi-apk 2
versionCode 260120101
versionName "0.1.2"
versionCode 260130401
versionName "0.1.3.3"
}
buildTypes {
release {
@@ -22,6 +22,7 @@ android {
wear1 {
dimension "minSdk"
// Use the defaultConfig value
minSdkVersion 23
}
wear2 {
dimension "minSdk"
@@ -36,8 +37,8 @@ android {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.android.support:wearable:2.1.0'
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.google.android.support:wearable:2.2.0'
implementation 'com.google.android.gms:play-services-wearable:11.8.0'
implementation 'com.android.support:percent:26.1.0'
implementation 'com.android.support:animated-vector-drawable:26.1.0'
@@ -45,6 +46,6 @@ dependencies {
implementation 'com.android.support:support-v4:26.1.0'
implementation 'com.android.support:recyclerview-v7:26.1.0'
implementation 'com.android.support:wear:26.1.0'
compileOnly 'com.google.android.wearable:wearable:2.1.0'
compile 'com.github.wendykierp:JTransforms:3.1'
compileOnly 'com.google.android.wearable:wearable:2.2.0'
api 'com.github.wendykierp:JTransforms:3.1'
}

View File

@@ -1,138 +0,0 @@
package de.tonifetzer.conductorswatch;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import de.tonifetzer.conductorswatch.bpmEstimation.AccelerometerData;
import de.tonifetzer.conductorswatch.bpmEstimation.AccelerometerWindowBuffer;
import de.tonifetzer.conductorswatch.bpmEstimation.BpmEstimator;
import de.tonifetzer.conductorswatch.network.SensorDataFileSender;
import de.tonifetzer.conductorswatch.network.SensorDataFileStreamer;
import de.tonifetzer.conductorswatch.utilities.ByteStreamWriter;
/**
* Created by toni on 13/11/17.
*/
public class Estimator implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private Sensor mGyroscope;
private Context mContext;
private AccelerometerWindowBuffer mAccelerometerWindowBuffer;
private BpmEstimator mBpmEstimator;
private ByteStreamWriter mByteStreamWriterAcc;
private ByteStreamWriter mByteStreamWriterGyro;
private SensorDataFileStreamer mStreamer;
private Timer mTimer = new Timer();
public Estimator(Context mContext){
this.mContext = mContext;
}
public void start() {
mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_FASTEST);
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(1024, 256);
mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000);
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (mAccelerometerWindowBuffer.isNextWindowReady()) {
double bpm = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedWindow());
for (OnBpmEstimatorListener listener : listeners) {
listener.onNewDataAvailable(bpm);
}
}
}
}, 0, 100);
mByteStreamWriterAcc = new ByteStreamWriter();
mByteStreamWriterGyro = new ByteStreamWriter();
mStreamer = new SensorDataFileStreamer(mContext);
new Thread(mStreamer).start();
}
public void stop() {
mTimer.cancel();
mTimer.purge();
mSensorManager.unregisterListener(this);
//send data and close the ByteStreamWriter
for (OnBpmEstimatorListener listener : listeners) {
//listener.onEstimationStopped(mByteStreamWriter.getByteArray());
}
mByteStreamWriterAcc.close();
mByteStreamWriterGyro.close();
mStreamer.close();
}
@Override
public void onSensorChanged(SensorEvent se) {
//TODO: at the moment this runs in main thread... since worker fragment runs also in main thread
//ca 200hz, every 5 to 6 ms we have an update
if (se.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) {
mAccelerometerWindowBuffer.add(new AccelerometerData(System.currentTimeMillis(), se.values[0], se.values[1], se.values[2]));
mByteStreamWriterAcc.writeSensor3D(Sensor.TYPE_LINEAR_ACCELERATION, se.values[0], se.values[1], se.values[2]);
mStreamer.sendByteArray(mByteStreamWriterAcc.getByteArray());
mByteStreamWriterAcc.reset();
}
if (se.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
//TODO: Rename AccelerometerData to SensorData3D
mByteStreamWriterGyro.writeSensor3D(Sensor.TYPE_GYROSCOPE, se.values[0], se.values[1], se.values[2]);
mStreamer.sendByteArray(mByteStreamWriterGyro.getByteArray());
mByteStreamWriterGyro.reset();
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
// do nothin
}
/**
* Interface for callback calculated bpm
*/
public interface OnBpmEstimatorListener {
void onNewDataAvailable(double bpm);
void onEstimationStopped(byte[] sensorData);
}
private List<OnBpmEstimatorListener> listeners = new CopyOnWriteArrayList<OnBpmEstimatorListener>();
public void add(OnBpmEstimatorListener listener){listeners.add(listener);}
public void remove(OnBpmEstimatorListener listener){listeners.remove(listener);}
}

View File

@@ -8,20 +8,23 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.support.wearable.activity.WearableActivity;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.TextView;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.tonifetzer.conductorswatch.network.SensorDataFileSender;
import de.tonifetzer.conductorswatch.ui.Croller;
import de.tonifetzer.conductorswatch.ui.TapBpm;
import de.tonifetzer.conductorswatch.utilities.Utils;
public class MainActivity extends WearableActivity implements WorkerFragment.OnFragmentInteractionListener, TapBpm.OnTapBpmListener {
public class MainActivity extends WearableActivity implements WorkerFragment.OnFragmentInteractionListener, TapBpm.OnTapBpmListener, Croller.onProgressChangedListener {
// member
private TextView mTextView;
private Croller mCroller;
private GestureDetector mDetector;
private boolean mModeRecord = false;
// connection to phone stuff
@@ -32,40 +35,39 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
private volatile boolean mReadyToSend = true;
// display center
private int mDisplayWidth;
private int mDisplayHeight;
private Point mDisplayCenter;
// saved Bpm to reset after recording
private int mMetronomBpm = 80;
private int mLastSendBPM = 80;
//threading
final ExecutorService mExecutorService = Executors.newCachedThreadPool();
// tapping
private Thread mTapBpmThread;
private TapBpm mTapBpm;
private boolean mTapRecognized = false;
private boolean mTappingStarted = false;
private int mTapBpmEstimation;
private Vibrator mVibrator;
//parameter for long press to start the worker
private Point mPreviousMovePoint;
private int mDistanceJitteringLongPress = 50; // in pixel
private int mLongPressDelay = 1200; // in Milliseconds
private boolean mLongPressHandlerActivated = false;
private final Handler mHandler = new Handler();
// runnable to switch between record mode and normal mode
private Runnable mLongPressed = new Runnable() {
public void run() {
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(100);
mLongPressHandlerActivated = true;
mModeRecord = !mModeRecord;
if (mModeRecord) {
WorkerFragment worker = new WorkerFragment();
//if we tapped, finish tapping
onTapFinished();
//provide the fragment with the bpm set
Bundle args = new Bundle();
args.putInt("bpm", mCroller.getProgress());
@@ -86,6 +88,7 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
transaction.commit();
} else {
//reset colors from red to green
mCroller.setProgressPrimaryColor(Color.parseColor("#158b69"));
mCroller.setBackCircleColor(Color.parseColor("#158b69"));
mTextView.setTextColor(Color.parseColor("#158b69"));
@@ -100,6 +103,7 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
}
};
//if we press the center button long enough, start mode switching runnable
private boolean onLongPressCustomized(MotionEvent ev) {
Point currentPoint = new Point((int) ev.getX(), (int) ev.getY());
@@ -115,27 +119,11 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
mHandler.postDelayed(mLongPressed, mLongPressDelay);
}
if ((ev.getAction() == MotionEvent.ACTION_MOVE) || (ev.getAction() == MotionEvent.ACTION_HOVER_MOVE)) {
/*
if (mPreviousMovePoint == null) {
mPreviousMovePoint = currentPoint;
} else {
int dx = Math.abs(currentPoint.x - mPreviousMovePoint.x);
int dy = Math.abs(currentPoint.y - mPreviousMovePoint.y);
int distance = (int) Math.sqrt(dx * dx + dy * dy);
if (distance > mDistanceJitteringLongPress) {
mHandler.removeCallbacks(mLongPressed);
return false;
}
}*/
}
if (ev.getAction() == MotionEvent.ACTION_UP) {
mHandler.removeCallbacks(mLongPressed);
if (mLongPressHandlerActivated) {
mLongPressHandlerActivated = false;
mPreviousMovePoint = null;
//mPreviousMovePoint = null;
return false;
}
return false;
@@ -144,7 +132,9 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
return false;
}
//animation that fills the circle slowly with color
private boolean onColorChanging(MotionEvent ev, int MainColor) {
//TODO: könnte man komplett in den Croller auslagern.
Point currentPoint = new Point((int) ev.getX(), (int) ev.getY());
//only works within the maincircle of the scroller
@@ -181,6 +171,7 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
return false;
}
//starts a runnable that estimates the bpm (for metronom) by tapping on the center circle
public boolean onTapForBpm(MotionEvent ev) {
Point currentPoint = new Point((int) ev.getX(), (int) ev.getY());
@@ -194,22 +185,16 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
}
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (!mTapRecognized) {
mTapBpm.addTimestamp(System.currentTimeMillis());
mTapRecognized = true;
}
mTapBpm.addTimestamp(System.currentTimeMillis());
}
if (ev.getAction() == MotionEvent.ACTION_UP) {
if (!mTapBpmThread.isAlive() && mTapRecognized) {
mTapBpmThread.start();
if (!mTappingStarted) {
mTappingStarted = true;
mCroller.setLabel("Tippe weiter");
mExecutorService.submit(mTapBpm);
}
mTapRecognized = false;
}
return false;
@@ -221,57 +206,20 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
setContentView(R.layout.activity_main);
//get display infos
mDisplayWidth = this.getResources().getDisplayMetrics().widthPixels;
mDisplayHeight = this.getResources().getDisplayMetrics().heightPixels;
mDisplayCenter = new Point((mDisplayWidth / 2), (mDisplayHeight / 2));
int displayWidth = this.getResources().getDisplayMetrics().widthPixels;
int displayHeight = this.getResources().getDisplayMetrics().heightPixels;
mDisplayCenter = new Point((displayWidth / 2), (displayHeight / 2));
mTapBpm = new TapBpm();
mTapBpm.add(this);
mTapBpmThread = new Thread(mTapBpm, "tapThread");
//ui and motion stuff
mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
mTapBpm = new TapBpm();
mTextView = findViewById(R.id.bpmText);
mCroller = findViewById(R.id.croller);
mTextView = (TextView) findViewById(R.id.bpmText);
//listener
mCroller.setOnProgressChangedListener(this);
mSender = new SensorDataFileSender(this);
// circular progress bar
mCroller = (Croller) findViewById(R.id.croller);
mCroller.setOnProgressChangedListener(new Croller.onProgressChangedListener() {
@Override
public void onProgressChanged(int currentBPM) {
if(currentBPM != mLastSendBPM){
mTextView.setText(Integer.toString(currentBPM));
mSender.sendMessage(UPDATE_PATH + ":" + mMetronomBpm + ":" + currentBPM);
mLastSendBPM = currentBPM;
}
if(mReadyToSend){
mReadyToSend = false;
new java.util.Timer().schedule(
new java.util.TimerTask() {
@Override
public void run() {
// your code here
mReadyToSend = true;
}
}, 100);
}
}
});
// detector for double clicks
mDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
public boolean onDoubleTap(MotionEvent ev) {
Log.d("Gesture", "onDoubleTap: " + ev.toString());
return true;
}
});
mTapBpm.add(this);
// Enables Always-on
setAmbientEnabled();
@@ -307,6 +255,8 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
@Override
protected void onStart() {
super.onStart();
mSender.wakeUpPhoneCall();
}
@Override
@@ -334,14 +284,11 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
mTapBpmEstimation = bpm;
synchronized (this) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mTapBpmEstimation > 0) {
mCroller.setProgress(mTapBpmEstimation);
mCroller.setLabel("Fertig");
mVibrator.vibrate(10);
}
runOnUiThread(() -> {
if (mTapBpmEstimation > 0) {
mCroller.setProgress(mTapBpmEstimation);
mCroller.setLabel("Fertig");
mVibrator.vibrate(20);
}
});
}
@@ -350,23 +297,35 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
@Override
public void onTapFinished() {
runOnUiThread(new Runnable() {
@Override
public void run() {
synchronized (this) {
runOnUiThread(() -> {
mCroller.setLabel("");
}
});
mTapBpm.clearTimestamps();
mTapRecognized = false;
/*
mTapBpmThread.interrupt();
try {
mTapBpmThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
mTapBpm.clearTimestamps();
mTappingStarted = false;
});
}
*/
}
@Override
public void onProgressChanged(int currentBPM) {
if(currentBPM != mLastSendBPM){
mTextView.setText(String.format(Locale.getDefault(), "%d", currentBPM));
mSender.sendMessage(UPDATE_PATH + ":" + mMetronomBpm + ":" + currentBPM);
mLastSendBPM = currentBPM;
}
//künstlicher delay, da es sonst so häufig aufgerufen wird und das sendMessage dann in die Hose geht.
if(mReadyToSend){
mReadyToSend = false;
new java.util.Timer().schedule(
new java.util.TimerTask() {
@Override
public void run() {
mReadyToSend = true;
}
}, 100);
}
}
}

View File

@@ -1,59 +0,0 @@
package de.tonifetzer.conductorswatch;
import android.os.Vibrator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Created by toni on 13/11/17.
*/
//TODO: implement the metronome similar to phone. since thread sleeping is no accurate enough
public class Metronome implements Runnable{
private volatile boolean mRunning = true;
private int mBPM;
public Metronome(int bpm){
mBPM = bpm;
}
@Override
public void run() {
while(mRunning){
for (OnMetronomeListener listener:listeners) {
listener.onNewClick();
}
try {
if(mBPM > 0){
Thread.sleep(60000 / mBPM);
} else {
Thread.sleep(60000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stop() {
mRunning = false;
}
/**
* Interface for callback metronome clicks
* I know that java has observable.. but this why it is more clean and easy
*/
public interface OnMetronomeListener {
void onNewClick();
}
private List<OnMetronomeListener> listeners = new CopyOnWriteArrayList<OnMetronomeListener>();
public void add(OnMetronomeListener listener){listeners.add(listener);}
public void remove(OnMetronomeListener listener){listeners.remove(listener);}
}

View File

@@ -1,9 +0,0 @@
package de.tonifetzer.conductorswatch;
public interface OnCrollerChangeListener {
void onProgressChanged(Croller croller, int progress);
void onStartTrackingTouch(Croller croller);
void onStopTrackingTouch(Croller croller);
}

View File

@@ -4,14 +4,19 @@ import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.app.Fragment;
import android.os.Vibrator;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import java.util.Timer;
import java.util.Vector;
import de.tonifetzer.conductorswatch.estimation.Estimator;
import de.tonifetzer.conductorswatch.ui.Croller;
import de.tonifetzer.conductorswatch.ui.Metronome;
/**
* A simple {@link Fragment} subclass.
@@ -23,14 +28,11 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
private OnFragmentInteractionListener mListener;
private Vector<Double> mBpmList;
private byte[] mSensorData;
private boolean mWorkerRunning = false;
private Estimator mEstimator;
private Metronome mMetronome;
private Thread mMetronomeThread;
private Timer mTimer;
private Vibrator mVibrator;
private TextView mTextView;
private Croller mCroller;
@@ -61,11 +63,7 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
mBpmList = new Vector<Double>();
// init metronome and listener
mMetronome = new Metronome(getArguments().getInt("bpm"));
mMetronome.add(this);
mMetronomeThread = new Thread(mMetronome, "metronomThread");
mVibrator = (Vibrator) this.getActivity().getSystemService(Context.VIBRATOR_SERVICE);
mTimer = new Timer();
//keep screen always on
this.getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -96,8 +94,8 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
// start the worker thread for bpm estimator
mEstimator.start();
// start the worker thread for metronom
mMetronomeThread.start();
// start the timer for metronom based on the predefined bpm
mTimer.scheduleAtFixedRate(new Metronome(getContext()), 0, 60000 / getArguments().getInt("bpm"));
// everything is running
mWorkerRunning = true;
@@ -113,7 +111,7 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
mEstimator.stop();
// stop the worker thread for metronom
mMetronome.stop();
mTimer.cancel();
//private listener with list of all estimated bpm
if (mListener != null) {
@@ -143,27 +141,21 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
@Override
public void onNewDataAvailable(double bpm) {
//TODO: what if multiple threads access mBpmList? put into synchronized? @frank fragen :D
//TODO: send this to smartphone
if(mWorkerRunning){
mBpmList.add(bpm);
if(bpm == -1){
//to stuff with UI. Write Text or make XX or something like that
}
mBpmList.add(bpm);
// we need this here, since ui elements can only be changed within activity thread and
// onNewDataAvailable lives inside the bpm estimation thread.
// TODO: is this really okay? also synchronized?
if(getActivity() != null) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
int tmpBPM = (int) (Math.round(mBpmList.lastElement()));
mTextView.setText(String.valueOf(tmpBPM));
mCroller.setProgress(tmpBPM);
}
getActivity().runOnUiThread(() -> {
int tmpBPM = (int) (Math.round(mBpmList.lastElement()));
mTextView.setText(String.valueOf(tmpBPM));
mCroller.setProgress(tmpBPM);
});
}
}

View File

@@ -1,85 +0,0 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.ArrayList;
/**
* Created by toni on 15/12/17.
*/
public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
private int mWindowSize;
private int mOverlapSize;
private int mOverlapCounter;
public AccelerometerWindowBuffer(int windowSize, int overlap){
mWindowSize = windowSize;
mOverlapSize = overlap;
mOverlapCounter = 1;
}
//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;
}
boolean r = super.add(ad);
if (size() > mWindowSize) {
removeRange(0, size() - mWindowSize);
}
++mOverlapCounter;
return r;
}
}
public boolean isNextWindowReady(){
if((size() > mWindowSize / 2) && mOverlapCounter > mOverlapSize){
mOverlapCounter = 1;
return true;
}
return false;
}
public AccelerometerWindowBuffer getFixedWindow(){
AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(mWindowSize, mOverlapSize);
synchronized (this){
for(AccelerometerData data : this){
other.add(new AccelerometerData(data));
}
}
return other;
}
public AccelerometerData getYongest() {
return get(size() - 1);
}
public AccelerometerData getOldest() {
return 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;
}
}

View File

@@ -0,0 +1,249 @@
package de.tonifetzer.conductorswatch.estimation;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerData;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
import de.tonifetzer.conductorswatch.network.SensorDataFileStreamer;
import de.tonifetzer.conductorswatch.utilities.ByteStreamWriter;
import de.tonifetzer.conductorswatch.utilities.Utils;
/**
* Created by toni on 13/11/17.
*/
public class Estimator implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private Sensor mRawAccelerometer;
private Context mContext;
private AccelerometerWindowBuffer mAccelerometerWindowBuffer;
private EstimatorAutoCorr mBpmEstimator;
private double mCurrentBpm;
private ByteStreamWriter mByteStreamWriterAcc;
private ByteStreamWriter mByteStreamWriterGyro;
private SensorDataFileStreamer mStreamer;
private Timer mTimer = new Timer();
public Estimator(Context mContext){
this.mContext = mContext;
this.mCurrentBpm = -1;
}
public void start() {
mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
mRawAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
mSensorManager.registerListener(this, mRawAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
mBpmEstimator = new EstimatorAutoCorr(mAccelerometerWindowBuffer, 0, 5000);
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (mAccelerometerWindowBuffer.isNextWindowReady()) {
LinkedList<Double> 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 (OnBpmEstimatorListener listener : listeners) {
listener.onNewDataAvailable(bpm); //135 gibt gute ergebnisse!
}
//update the windowSize and updaterate depending on current bpm
//updateWindowSizeAndOverlap(bpm);
}
}
}, 0, 100);
mByteStreamWriterAcc = new ByteStreamWriter();
mByteStreamWriterGyro = new ByteStreamWriter();
mStreamer = new SensorDataFileStreamer(mContext);
new Thread(mStreamer).start();
}
public void stop() {
mTimer.cancel();
mTimer.purge();
mSensorManager.unregisterListener(this);
//send data and close the ByteStreamWriter
for (OnBpmEstimatorListener listener : listeners) {
//listener.onEstimationStopped(mByteStreamWriter.getByteArray());
}
mByteStreamWriterAcc.close();
mByteStreamWriterGyro.close();
mStreamer.close();
}
@Override
public void onSensorChanged(SensorEvent se) {
//TODO: at the moment this runs in main thread... since worker fragment runs also in main thread
//ca 200hz, every 5 to 6 ms we have an update
if (se.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) {
mAccelerometerWindowBuffer.add(new AccelerometerData(System.currentTimeMillis(), se.values[0], se.values[1], se.values[2]));
mByteStreamWriterAcc.writeSensor3D(Sensor.TYPE_LINEAR_ACCELERATION, se.values[0], se.values[1], se.values[2]);
mStreamer.sendByteArray(mByteStreamWriterAcc.getByteArray());
mByteStreamWriterAcc.reset();
}
if (se.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mByteStreamWriterGyro.writeSensor3D(Sensor.TYPE_ACCELEROMETER, se.values[0], se.values[1], se.values[2]);
mStreamer.sendByteArray(mByteStreamWriterGyro.getByteArray());
mByteStreamWriterGyro.reset();
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
// do nothin
}
/**
* Interface for callback calculated bpm
*/
public interface OnBpmEstimatorListener {
void onNewDataAvailable(double bpm);
void onEstimationStopped(byte[] sensorData);
}
private List<OnBpmEstimatorListener> listeners = new CopyOnWriteArrayList<OnBpmEstimatorListener>();
public void add(OnBpmEstimatorListener listener){listeners.add(listener);}
public void remove(OnBpmEstimatorListener listener){listeners.remove(listener);}
/**
* Simple function that sets die windowSize and Overlap time to a specific value
* depending on the currentBPM. Nothing rly dynamical. However, should work out
* for our purposes.
* @param bpm
*/
private void updateWindowSizeAndOverlap(double bpm){
//round to nearest tenner. this limits the number of windowsize and overlap changes.
int newBpmRounded = (int) Math.round(bpm / 10.0) * 10;
int curBpmRounded = (int) Math.round(mCurrentBpm / 10.0) * 10;
//TODO: i guess this is not the best method.. if the default sizes always produces -1, we run into problems
if(bpm != -1 && curBpmRounded != newBpmRounded){
int overlap_ms = 60000 / newBpmRounded;
int window_ms = overlap_ms * 5;
//idea: wenn man mehrere fenster parallel laufen lässt und beobachtet, müsste das kleinste fenster die tempowechsel eigentlich am
// besten mitbekommen. dieses fenster dann bestimmen lassen?
mAccelerometerWindowBuffer.setWindowSize(window_ms);
mAccelerometerWindowBuffer.setOverlapSize(overlap_ms);
} else if (bpm == -1){
//if bpm is -1 due to a non-classification, reset to default.
//idea: anstatt auf einen festen wert zu setzen, könnte man das fenster dann auch einfach ein wenig größer / kleiner machen.
mAccelerometerWindowBuffer.setWindowSize(3000);
mAccelerometerWindowBuffer.setOverlapSize(750);
}
mCurrentBpm = bpm;
}
}

View File

@@ -0,0 +1,216 @@
package de.tonifetzer.conductorswatch.estimation;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerInterpolator;
import de.tonifetzer.conductorswatch.estimation.dsp.AutoCorrelation;
import de.tonifetzer.conductorswatch.estimation.dsp.PeakDetector;
import de.tonifetzer.conductorswatch.utilities.MovingFilter;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.LinkedList;
/**
* Created by toni on 17/12/17.
*/
public class EstimatorAutoCorr {
private AccelerometerWindowBuffer mBuffer;
private double mSampleRate_ms;
private LinkedList<Double> mBpmHistory_X;
private LinkedList<Double> mBpmHistory_Y;
private LinkedList<Double> mBpmHistory_Z;
private LinkedList<Double> mBpmHistory;
private int mResetCounter;
private int mResetLimit_ms;
private MovingFilter mMvg;
//private SimpleKalman mKalman;
EstimatorAutoCorr(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();
}
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;
}
//TODO: ist doch käse das zu interpolieren, das mach ich ja für jedes mal estimation.
//TODO: eigentlich muss ich ja nur das größte interpolieren und dann supwindows nehmen.
AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate);
//are we conducting?
//just look at the newest 512 samples
//List<AccelerometerData> 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);
PeakDetector pX = new PeakDetector(xAutoCorr, peakWidth, 0.1f, 0, false);
PeakDetector pY = new PeakDetector(yAutoCorr, peakWidth, 0.1f, 0, false);
PeakDetector pZ = new PeakDetector(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((double) (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(PeakDetector peaksX, PeakDetector peaksY, PeakDetector 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 PeakDetector 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();
}
}
}

View File

@@ -0,0 +1,31 @@
package de.tonifetzer.conductorswatch.estimation;
import java.util.LinkedList;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
import de.tonifetzer.conductorswatch.utilities.MovingFilter;
public class EstimatorDistCorr {
private AccelerometerWindowBuffer mBuffer;
private double mSampleRate_ms;
private LinkedList<Double> mBpmHistory;
private int mResetCounter;
private int mResetLimit_ms;
private MovingFilter mMvg;
EstimatorDistCorr(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
mBuffer = windowBuffer;
mSampleRate_ms = sampleRate_ms;
mBpmHistory = new LinkedList<>();
mResetCounter = 0;
mResetLimit_ms = resetAfter_ms;
mMvg = new MovingFilter(2);
}
//hier gehts weiter :)
}

View File

@@ -1,4 +1,4 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
package de.tonifetzer.conductorswatch.estimation.accelerometer;
/**
* Created by toni on 15/12/17.

View File

@@ -0,0 +1,159 @@
package de.tonifetzer.conductorswatch.estimation.accelerometer;
import java.util.Arrays;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerData;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindow;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
import de.tonifetzer.conductorswatch.utilities.Utils;
/**
* Created by toni on 16/12/17.
*/
public class AccelerometerInterpolator {
private double[] mX;
private double[] mY;
private double[] mZ;
private long[] mTsInterp;
private int mSize;
public AccelerometerInterpolator(AccelerometerWindowBuffer ab, double sampleRate_ms){
mSize = (int) ((ab.getYongest().ts - (ab.getOldest().ts - (long) sampleRate_ms)) / (long) sampleRate_ms);
mTsInterp = new long[mSize];
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(int idx){
return mX[idx];
}
public double getY(int idx){
return mY[idx];
}
public double getZ(int idx){
return mZ[idx];
}
public double[] getX(){
return mX;
}
public double[] getY(){
return mY;
}
public double[] getZ(){
return mZ;
}
public int size(){
return mSize;
}
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);
}
public static AccelerometerWindow interpolate(AccelerometerWindow data){
//calculate samplerate
double[] rawTimestamps = data.getTs_D();
double sampleRate_ms = Math.round(Utils.mean(Utils.diff(rawTimestamps)));
//create interpolated timestamps
double[] interpolatedTimestamps;
int size = (int) ((data.getLast().ts - (data.getFirst().ts - (long) sampleRate_ms)) / (long) sampleRate_ms);
interpolatedTimestamps = new double[size];
int j = 0;
for(double i = data.getFirst().ts; i <= data.getLast().ts; i += sampleRate_ms){
interpolatedTimestamps[j++] = i;
}
//interpolate the single axis separately
double[] interpolatedX = interpLinear(rawTimestamps, data.getX(), interpolatedTimestamps);
double[] interpolatedY = interpLinear(rawTimestamps, data.getY(), interpolatedTimestamps);
double[] interpolatedZ = interpLinear(rawTimestamps, data.getZ(), interpolatedTimestamps);
//merge everything back into an ArrayList
AccelerometerWindow interpolatedData = new AccelerometerWindow(size);
for(int i = 0; i < size; ++i){
interpolatedData.add(new AccelerometerData((long) rawTimestamps[i], interpolatedX[i], interpolatedY[i], interpolatedZ[i]));
}
return interpolatedData;
}
}

View File

@@ -0,0 +1,45 @@
package de.tonifetzer.conductorswatch.estimation.accelerometer;
import java.util.ArrayList;
import java.util.LinkedList;
public class AccelerometerWindow extends ArrayList<AccelerometerData> {
public AccelerometerWindow(int size){
super(size);
}
public AccelerometerWindow(AccelerometerWindow other){
super(other);
}
public AccelerometerWindow(LinkedList<AccelerometerData> other){
super(other);
}
public AccelerometerData getLast() {
//TODO: check if list is empty! this causes indexoutofbounce
synchronized (this){
return super.get(size() - 1);
}
}
public AccelerometerData getFirst() {
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_L(){
return this.stream().mapToLong(d -> d.ts).toArray();
}
public double[] getTs_D(){
return this.stream().mapToDouble(d -> d.ts).toArray();
}
}

View File

@@ -0,0 +1,148 @@
package de.tonifetzer.conductorswatch.estimation.accelerometer;
import java.util.ArrayList;
/**
* Created by toni on 15/12/17.
*/
public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
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 = (double) ((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;
}
}
}

View File

@@ -0,0 +1,110 @@
package de.tonifetzer.conductorswatch.estimation.accelerometer;
import java.util.ArrayList;
import java.util.LinkedList;
public class AccelerometerWindowContainer {
private int mWindowSize;
private int mOverlapSize;
private int mOverlapCounter;
//data arrays that provide fixed windows for calculation
private AccelerometerWindow mRawData;
private AccelerometerWindow mInterpolatedData;
//buffer that is permanently updated through incoming sensor data
private LinkedList<AccelerometerData> mBuffer;
public AccelerometerWindowContainer(int windowSize_ms, int overlap_ms){
mWindowSize = windowSize_ms;
mOverlapSize = overlap_ms;
mOverlapCounter = 0;
mBuffer = new LinkedList<>();
}
public boolean add(AccelerometerData ad){
//TODO: add exception handling. falseArgument if ad has no numeric x,y,z
synchronized (this){
//do not add duplicates!
if(!mBuffer.isEmpty() && mBuffer.getLast().equals(ad)){
return false;
}
// current - last to increment overlap time
if(!mBuffer.isEmpty()){
mOverlapCounter += ad.ts - mBuffer.getLast().ts;
}
//add element
boolean r = mBuffer.add(ad);
removeOldElements();
return r;
}
}
private void removeOldElements(){
synchronized (this) {
if (!mBuffer.isEmpty()) {
if ((mBuffer.getLast().ts - mBuffer.getFirst().ts) > mWindowSize) {
while(mBuffer.getFirst().ts < mBuffer.getLast().ts - mWindowSize){
mBuffer.remove(0);
}
}
}
}
}
public boolean isNextWindowReady(){
if(!mBuffer.isEmpty()){
if(((mBuffer.getFirst().ts - mBuffer.getLast().ts) > mWindowSize / 4) && mOverlapCounter > mOverlapSize){
mOverlapCounter = 0;
//fill the data arrays - we use a shallow copy, as the values are not touched.
mRawData = new AccelerometerWindow(mBuffer);
mInterpolatedData = AccelerometerInterpolator.interpolate(mRawData);
return true;
}
}
return false;
}
public ArrayList<AccelerometerData> getRawData(){
return mRawData;
}
public ArrayList<AccelerometerData> getInterpolatedData(){
return mInterpolatedData;
}
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;
}
public void clear(){
synchronized (this){
mBuffer.clear();
mRawData.clear();
mInterpolatedData.clear();
this.mOverlapCounter = 0;
}
}
}

View File

@@ -1,4 +1,4 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
package de.tonifetzer.conductorswatch.estimation.dsp;
import org.jtransforms.fft.DoubleFFT_1D;
import de.tonifetzer.conductorswatch.utilities.Utils;

View File

@@ -0,0 +1,67 @@
package de.tonifetzer.conductorswatch.estimation.dsp;
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindow;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
/**
* Created by toni on 06/12/18.
*/
public class DistanceCorrelation {
//TODO: remove bad peaks found at the very beginning and end of the signal
private static int mMaxLag;
private double[] mCorr;
public DistanceCorrelation(AccelerometerWindow data, int maxLag){
mMaxLag = maxLag;
mCorr = calc(data);
}
public double[] getCorr(){
return mCorr;
}
private double[] calc(AccelerometerWindow data){
if(mMaxLag < 1){
throw new RuntimeException("maxlag has to be greater 1");
}
//init
int n = data.size();
int lag_size = Math.min(mMaxLag, n - 1);
double[] corr = new double[lag_size + 1];
//do the math
for(int j = 0; j <= lag_size; ++j){
double[] dist = new double[n - Math.abs(j)];
int idx = 0;
for (int i = j; i < n; ++i){
dist[idx] = Utils.getDistance(data.get(i).x, data.get(i).y, data.get(i).z, data.get(i-j).x, data.get(i-j).y, data.get(i-j).z);
++idx;
}
corr[j] = Utils.geometricMeanLog(dist);
}
//to [0, 1]
DoubleSummaryStatistics corrStat = Arrays.stream(corr).summaryStatistics();
double corMaxVal = corrStat.getMax();
for(int k = 0; k < corr.length; ++k){
corr[k] = ((corr[k] * (-1)) / corMaxVal) + 1;
}
// mirror corr(2:512) and put it in front
double[] output = new double[(2 * lag_size) + 1];
System.arraycopy(corr, 0, output, lag_size, lag_size + 1); // +1 to place the 1.0 in the middle of correlation
Utils.reverse(corr);
System.arraycopy(corr, 0, output, 0, lag_size);
return output;
}
}

View File

@@ -0,0 +1,206 @@
package de.tonifetzer.conductorswatch.estimation.dsp;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.LinkedList;
/**
* Created by toni on 15/12/17.
*/
public class PeakDetector {
private LinkedList<Integer> mPeaksIdx; //provide the idx within the given data array
private LinkedList<Integer> mPeaksPos; //the real position within the data-rang e.g. lag -1024 to 1024
private LinkedList<Double> 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 PeakDetector(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<Integer> getPeaksIdx() {
return mPeaksIdx;
}
public int[] getPeaksIdxAsArray() {
return mPeaksIdx.stream().mapToInt(i->i).toArray();
}
public LinkedList<Integer> getPeaksPos() {
return mPeaksPos;
}
public double[] getPeaksPosAsArray() {
return mPeaksPos.stream().mapToDouble(i -> i).toArray();
}
public LinkedList<Double> 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);
}
}

View File

@@ -152,12 +152,15 @@ public class SensorDataFileStreamer implements Runnable{
}
public void close(){
try {
mRunning = false;
mOutputStream.close();
mOutputStreamOpened = false;
} catch (IOException e) {
e.printStackTrace();
if(mChannelOpenend && mNode != null){
try {
mRunning = false;
mOutputStream.close();
mOutputStreamOpened = false;
} catch (IOException e) {
//e.printStackTrace();
Log.d(TAG, "Closing the outputstream failed!");
}
}
}
}

View File

@@ -1,7 +1,6 @@
package de.tonifetzer.conductorswatch;
package de.tonifetzer.conductorswatch.ui;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
@@ -73,6 +72,12 @@ public class Croller extends View {
this.mProgressChangeListener = mProgressChangeListener;
}
public interface OnCrollerChangeListener {
void onProgressChanged(Croller croller, int progress);
void onStartTrackingTouch(Croller croller);
void onStopTrackingTouch(Croller croller);
}
public void setOnCrollerChangeListener(OnCrollerChangeListener mCrollerChangeListener) {
this.mCrollerChangeListener = mCrollerChangeListener;
}

View File

@@ -0,0 +1,42 @@
package de.tonifetzer.conductorswatch.ui;
import android.content.Context;
import android.os.Vibrator;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Created by toni on 13/11/17.
*/
public class Metronome extends TimerTask {
private Vibrator mVibrator;
public Metronome(Context context){
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
@Override
public void run() {
mVibrator.vibrate(10);
for (OnMetronomeListener listener:listeners) {
listener.onNewClick();
}
}
/**
* Interface for callback metronome clicks
* I know that java has observable.. but this why it is more clean and easy
*/
public interface OnMetronomeListener {
void onNewClick();
}
private List<OnMetronomeListener> listeners = new CopyOnWriteArrayList<OnMetronomeListener>();
public void add(OnMetronomeListener listener){listeners.add(listener);}
public void remove(OnMetronomeListener listener){listeners.remove(listener);}
}

View File

@@ -1,6 +1,4 @@
package de.tonifetzer.conductorswatch;
import android.util.Log;
package de.tonifetzer.conductorswatch.ui;
import java.util.List;
import java.util.Vector;
@@ -13,36 +11,36 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class TapBpm implements Runnable {
private Vector<Long> mReceivedTabs = new Vector<Long>();
private int mBpmTapped;
@Override
public void run() {
int breakCounter = 2000;
mBpmTapped = 0;
int bpmTapped = 0;
int calcNewBpmCounter = 3;
do{
if(mBpmTapped > 0){
breakCounter = 2 * (60000 / mBpmTapped);
}
if (mReceivedTabs.size() > calcNewBpmCounter) {
Long sumDifferenceMs = 0l;
for (int i = 0; i < mReceivedTabs.size() -1; ++i) {
sumDifferenceMs += mReceivedTabs.get(i + 1)- mReceivedTabs.get(i);
}
mBpmTapped = (int) (60000 / (sumDifferenceMs / (mReceivedTabs.size() - 1)));
for (TapBpm.OnTapBpmListener listener:listeners) {
listener.onNewTapEstimation(mBpmTapped);
if(!mReceivedTabs.isEmpty()){
do{
if(bpmTapped > 0){
breakCounter = 2 * (60000 / bpmTapped);
}
//only update everytime a new timestamp arrives
++calcNewBpmCounter;
}
}while(System.currentTimeMillis() - mReceivedTabs.lastElement() < breakCounter);
if (mReceivedTabs.size() > calcNewBpmCounter) {
long sumDifferenceMs = 0L;
for (int i = 0; i < mReceivedTabs.size() -1; ++i) {
sumDifferenceMs += mReceivedTabs.get(i + 1)- mReceivedTabs.get(i);
}
bpmTapped = (int) (60000 / (sumDifferenceMs / (mReceivedTabs.size() - 1)));
for (TapBpm.OnTapBpmListener listener:listeners) {
listener.onNewTapEstimation(bpmTapped);
}
//only update everytime a new timestamp arrives
++calcNewBpmCounter;
}
}while(System.currentTimeMillis() - mReceivedTabs.lastElement() < breakCounter);
}
for (TapBpm.OnTapBpmListener listener:listeners) {
listener.onTapFinished();
@@ -50,11 +48,11 @@ public class TapBpm implements Runnable {
}
public void addTimestamp(Long ts){
mReceivedTabs.add(ts);
mReceivedTabs.add(ts);
}
public void clearTimestamps(){
mReceivedTabs.clear();
mReceivedTabs.clear();
}
/**

View File

@@ -4,13 +4,35 @@ import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
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));
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
public static double getDistance(double x1, double y1, double z1, double x2, double y2, double z2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2));
}
public static void reverse(double[] array) {
if (array == null) {
return;
}
int i = 0;
int j = array.length - 1;
double tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
}
public static double sqr(double x) {
@@ -37,7 +59,7 @@ public class Utils {
return sum;
}
public static double sum (LinkedList<Double> data){
public static double sum (List<Double> data){
double sum = 0;
for (int i = 0; i < data.size(); i++) {
sum += data.get(i).doubleValue();
@@ -81,11 +103,11 @@ public class Utils {
return (double) sum(data) / (double) data.length;
}
public static double mean(LinkedList<Double> data){
public static double mean(List<Double> data){
return sum(data) / data.size();
}
public static double median(LinkedList<Double> data){
public static double median(List<Double> data){
data.sort(Comparator.naturalOrder());
double median;
@@ -97,6 +119,18 @@ public class Utils {
return median;
}
public static double mad(List<Double> data){
double median = median(data);
List<Double> 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];
@@ -106,6 +140,17 @@ public class Utils {
return Math.pow(sum, 1.0 / data.length);
}
public static double geometricMeanLog(double[] data){
double GM_log = 0.0d;
for (int i = 0; i < data.length; ++i) {
if (data[i] == 0.0d) {
return 0.0d;
}
GM_log += Math.log(data[i]);
}
return Math.exp(GM_log / data.length);
}
public static int intersectionNumber(double[] signal, double border){
int cnt = 0;
boolean isSmallerValue = false;
@@ -173,4 +218,17 @@ public class Utils {
return px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
public static void removeOutliersZScore(List<Double> data, double score) {
if(!data.isEmpty()){
double median = median(data);
double mad = mad(data);
for(Iterator<Double> it = data.iterator(); it.hasNext(); ){
double M = Math.abs((0.6745 * (it.next() - median)) / mad);
if (M > score){ it.remove(); }
}
}
}
}

View File

@@ -16,7 +16,7 @@
android:layout_height="match_parent">
<de.tonifetzer.conductorswatch.Croller
<de.tonifetzer.conductorswatch.ui.Croller
android:id="@+id/croller"
android:layout_width="match_parent"
android:layout_height="match_parent"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -3,11 +3,14 @@
buildscript {
repositories {
google()
maven {
url "https://maven.google.com"
}
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.3.0'
// NOTE: Do not place your application dependencies here; they belong
@@ -17,8 +20,11 @@ buildscript {
allprojects {
repositories {
google()
maven {
url "https://maven.google.com"
}
jcenter()
google()
}
}

View File

@@ -10,6 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
android.enableD8=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit

View File

@@ -1,6 +1,6 @@
#Mon Nov 13 10:12:40 CET 2017
#Sun Jan 27 12:15:23 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

View File

@@ -1,4 +1,6 @@
import bpmEstimation.*;
import uk.me.berndporr.iirj.Butterworth;
import utilities.Plot;
import utilities.Utils;
import java.awt.*;
@@ -6,7 +8,10 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Vector;
import java.util.stream.IntStream;
@@ -16,32 +21,64 @@ import java.util.stream.IntStream;
public class Main {
public static void main(String [ ] args) {
File folder = new File("/home/toni/Documents/programme/dirigent/measurements/wearR");
//File folder = new File("/home/toni/Documents/programme/dirigent/measurements/2017.06/lgWear");
//File folder = new File("/home/toni/Documents/programme/dirigent/measurements/peter_failed");
File folder = new File("/home/toni/Documents/programme/dirigent/measurements/2018.06/frank/mSensorTest");
File[] listOfFiles = folder.listFiles();
Arrays.sort(listOfFiles);
Utils.ShowPNG windowRaw = new Utils.ShowPNG();
Utils.ShowPNG windowAuto = new Utils.ShowPNG();
Utils.ShowPNG windowPeaksX = new Utils.ShowPNG();
Utils.ShowPNG windowPeaksY = new Utils.ShowPNG();
Utils.ShowPNG windowPeaksZ = new Utils.ShowPNG();
//calc results
BpmHistory historyAll = new BpmHistory();
BpmHistory historyMag = new BpmHistory();
BpmHistory history3D = new BpmHistory();
// iterate trough files in measurements folder
for (File file : listOfFiles) {
if (file.isFile() && file.getName().contains(".csv")) {
AccelerometerWindowBuffer accWindowBuffer = new AccelerometerWindowBuffer(1024, 256);
BpmEstimator bpmEstimator = new BpmEstimator(accWindowBuffer, 0, 5000);
AccelerometerWindowBuffer accWindowBuffer = new AccelerometerWindowBuffer(6000, 875);
BpmEstimator bpmEstimator = new BpmEstimator(accWindowBuffer, 4, 50000);
//read the file line by line
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
//read the first three lines and print out what file it is!
String comment = br.readLine();
String groundTruthLine = br.readLine();
br.readLine();
System.out.println(comment);
System.out.println(file.getName());
//load ground truth file
final long startTs = Long.parseLong(br.readLine().split(";")[0]);
String gtFile = groundTruthLine.substring(groundTruthLine.indexOf(':') + 2);
Utils.GroundTruthData gtData = new Utils.GroundTruthData();
double gtCurValue = 0d;
if (gtFile.contains(".csv")) {
try (BufferedReader gtBr = new BufferedReader(new FileReader("../../measurements/2018.06/gt_toni/" + gtFile))) {
for (String gtLine; (gtLine = gtBr.readLine()) != null; ) {
gtData.setValuesFromString(gtLine);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
}
} else {
gtData.setSingleBPM(Double.valueOf(gtFile));
}
//read sensor measurements line by line
for (String line; (line = br.readLine()) != null; ) {
// process the line.
String[] measurement = line.split(";");
//if linear acc
if(measurement[1].equals("2")){
long ts = Long.parseLong(measurement[0]);
long ts = 0;
if (measurement[1].equals("3")) {
ts = Long.parseLong(measurement[0]);
double x = Double.parseDouble(measurement[2]);
double y = Double.parseDouble(measurement[3]);
double z = Double.parseDouble(measurement[4]);
@@ -49,109 +86,99 @@ public class Main {
}
//do calculation stuff
if(accWindowBuffer.isNextWindowReady()){
if (accWindowBuffer.isNextWindowReady()) {
double curBpm = bpmEstimator.estimate();
//System.out.println("BPM: " + curBpm);
LinkedList<Double> bpmList = new LinkedList<>();
// Calculate the BPM for different window sizes
double bpm60 = bpmEstimator.estimate();
double bpm85 = bpmEstimator.estimate(3500, 875);
double bpm110 = bpmEstimator.estimate(2600, 875);
double bpm135 = bpmEstimator.estimate(2000, 875);
double bpm160 = bpmEstimator.estimate(1600, 875);
double bpm200 = bpmEstimator.estimate(1200, 875);
//System.out.println("--------------------------------------------------");
bpmList.add(bpm60);
bpmList.add(bpm85);
bpmList.add(bpm110);
bpmList.add(bpm135);
bpmList.add(bpm160);
bpmList.add(bpm200);
while (bpmList.remove(Double.valueOf(-1))) {
}
Utils.removeOutliersZScore(bpmList, 3.4);
double bpmMean = Utils.mean(bpmList);
double magMean = bpmEstimator.getMagnitudeMean();
double bpmDist = bpmEstimator.getDistEstimation();
//double bpmSingle = bpmEstimator.getBestSingleAxis();
double bpmAllAverage = bpmEstimator.getAverageOfAllWindows();
//System.out.println( ts + " all: " + Math.round(bpmMean) + " avg_all: " + Math.round(bpmAllAverage) + " 3D: " + Math.round(bpmDist));
//System.out.println(" ");
//calc error using ground truth
long curTS = accWindowBuffer.getYongest().ts - startTs;
int idx = 0;
while (curTS > gtData.getTimestamp(idx) && idx < gtData.getSize() - 1) {
++idx;
}
gtCurValue = gtData.getBPM(idx);
//fill histories
historyAll.add(bpmAllAverage - gtCurValue);
historyMag.add(magMean - gtCurValue);
history3D.add(bpmDist - gtCurValue);
AccelerometerInterpolator acInterp = new AccelerometerInterpolator(accWindowBuffer, 5);
//print raw x,y,z
double[] dTs = IntStream.range(0, accWindowBuffer.getTs().length).mapToDouble(i -> accWindowBuffer.getTs()[i]).toArray();
double[] dTsInterp = IntStream.range(0, acInterp.getTs().length).mapToDouble(i -> acInterp.getTs()[i]).toArray();
Plot plotRaw = Plot.plot(Plot.plotOpts().
title("Raw Acc Data").
legend(Plot.LegendFormat.BOTTOM)).
series("x", Plot.data().xy(dTsInterp, acInterp.getX()), Plot.seriesOpts().color(Color.RED)).
series("y", Plot.data().xy(dTsInterp, acInterp.getY()), Plot.seriesOpts().color(Color.BLUE)).
series("z", Plot.data().xy(dTsInterp, acInterp.getZ()), Plot.seriesOpts().color(Color.GREEN));
windowRaw.set(plotRaw.draw());
//auto corr
double[] xAutoCorr = new AutoCorrelation(acInterp.getX(), 512).getCorr();
double[] yAutoCorr = new AutoCorrelation(acInterp.getY(), 512).getCorr();
double[] zAutoCorr = new AutoCorrelation(acInterp.getZ(), 512).getCorr();
//print autocorr
int[] tmp = IntStream.rangeClosed(-((xAutoCorr.length - 1)/2), ((xAutoCorr.length - 1)/2)).toArray();
double[] rangeAuto = IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray();
Plot plotCorr = Plot.plot(Plot.plotOpts().
title("Auto Correlation").
legend(Plot.LegendFormat.BOTTOM)).
series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)).
series("y", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.BLUE)).
series("z", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.GREEN));
windowAuto.set(plotCorr.draw());
Peaks pX = new Peaks(xAutoCorr, 50, 0.1f, 0, false);
LinkedList<Integer> peaksX = pX.getPeaksIdx();
double[] dPeaksXX = IntStream.range(0, peaksX.size()).mapToDouble(i -> (peaksX.get(i) - 512)).toArray();//peaks.stream().mapToDouble(i->i).toArray();
double[] dPeaksXY = IntStream.range(0, peaksX.size()).mapToDouble(i -> (xAutoCorr[peaksX.get(i)])).toArray();
Plot plotPeaksX = Plot.plot(Plot.plotOpts().
title("Peak Detection on X").
legend(Plot.LegendFormat.BOTTOM)).
series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)).
series("Peaks", Plot.data().xy(dPeaksXX, dPeaksXY), Plot.seriesOpts().color(Color.CYAN).
marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE));
windowPeaksX.set(plotPeaksX.draw());
Peaks pY = new Peaks(yAutoCorr, 50, 0.1f, 0, false);
LinkedList<Integer> peaksY = pY.getPeaksIdx();
double[] dPeaksYX = IntStream.range(0, peaksY.size()).mapToDouble(i -> (peaksY.get(i) - 512)).toArray();//peaks.stream().mapToDouble(i->i).toArray();
double[] dPeaksYY = IntStream.range(0, peaksY.size()).mapToDouble(i -> (yAutoCorr[peaksY.get(i)])).toArray();
Plot plotPeaksY = Plot.plot(Plot.plotOpts().
title("Peak Detection on Y").
legend(Plot.LegendFormat.BOTTOM)).
series("x", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.RED)).
series("Peaks", Plot.data().xy(dPeaksYX, dPeaksYY), Plot.seriesOpts().color(Color.CYAN).
marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE));
windowPeaksY.set(plotPeaksY.draw());
Peaks pZ = new Peaks(zAutoCorr, 50, 0.1f, 0, false);
LinkedList<Integer> peaksZ = pZ.getPeaksIdx();
double[] dPeaksZX = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (peaksZ.get(i) - 512)).toArray();//peaks.stream().mapToDouble(i->i).toArray();
double[] dPeaksZY = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (zAutoCorr[peaksZ.get(i)])).toArray();
Plot plotPeaksZ = Plot.plot(Plot.plotOpts().
title("Peak Detection on Z").
legend(Plot.LegendFormat.BOTTOM)).
series("x", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.RED)).
series("Peaks", Plot.data().xy(dPeaksZX, dPeaksZY), Plot.seriesOpts().color(Color.CYAN).
marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE));
windowPeaksZ.set(plotPeaksZ.draw());
//fill hols improve peaks
//estimate bpm between detected peaks
//System.out.println("BPM-X: " + pX.getBPM(bpmEstimator.getSampleRate_ms()));
//System.out.println("BPM-Y: " + pY.getBPM(bpmEstimator.getSampleRate_ms()));
//System.out.println("BPM-Z: " + pZ.getBPM(bpmEstimator.getSampleRate_ms()));
//todo: kleiner fenstergrößen testen. so ist doch etwas langsam auf der Uhr.
if(true){
System.out.println("all: " + bpmAllAverage);
System.out.println("mag: " + magMean);
System.out.println("3D: " + bpmDist);
System.out.println("GT: " + gtCurValue);
System.out.println(" ");
}
int dummyForBreakpoint = 0;
}
}
// line is not visible here.
double meanBPM = bpmEstimator.getMeanBpm();
double medianBPM = bpmEstimator.getMedianBPM();
//double meanBPM = bpmEstimator.getMeanBpm();
//double medianBPM = bpmEstimator.getMedianBPM();
//System.out.println("MEAN BPM: " + Math.round(meanBPM));
System.out.println("MEDIAN BPM: " + Math.round(medianBPM));
//System.out.println("MEDIAN BPM: " + Math.round(medianBPM));
if (Utils.DEBUG_MODE) {
bpmEstimator.closeDebugWindows();
}
} catch (IOException e) {
e.printStackTrace();
}
//print overall stats for a single data series
System.out.println("all: " + historyAll.getMean() + "(" + historyAll.getStd() + ")");
System.out.println("mag: " + historyMag.getMean() + "(" + historyMag.getStd() + ")");
System.out.println(" 3D: " + history3D.getMean() + "(" + history3D.getStd() + ")");
System.out.println(" ");
history3D.clear();
historyMag.clear();
historyAll.clear();
}
// try {
// System.in.read();
// } catch (IOException e) {
// e.printStackTrace();
// }
}
}
}

View File

@@ -11,11 +11,12 @@ public class AccelerometerInterpolator {
private double[] mY;
private double[] mZ;
private long[] mTsInterp;
private int size;
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];
this.size = (int) ((ab.getYongest().ts - (ab.getOldest().ts - (long) sampleRate_ms)) / (long) sampleRate_ms);
mTsInterp = new long[size];
int j = 0;
for(long i = ab.getOldest().ts; i <= ab.getYongest().ts; i += sampleRate_ms){
mTsInterp[j++] = i;
@@ -42,6 +43,10 @@ public class AccelerometerInterpolator {
return mZ;
}
public int size(){
return size;
}
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");

View File

@@ -2,48 +2,105 @@ package bpmEstimation;
import utilities.Utils;
import java.util.ArrayList;
import java.util.List;
/**
* Created by toni on 15/12/17.
*/
public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
private static int mWindowSize;
private static int mOverlapSize;
private int mOverlapCounter;
private final int mWindowSize; // in ms
private final int mOverlapSize; // in ms
private long mOverlapCounter;
public AccelerometerWindowBuffer(int windowSize, int overlap){
mWindowSize = windowSize;
mOverlapSize = overlap;
mOverlapCounter = 1;
mOverlapCounter = 0;
}
public AccelerometerWindowBuffer(List<AccelerometerData> list, int overlap){
mWindowSize = list.size();
mOverlapSize = overlap;
mOverlapCounter = 0;
super.addAll(list);
}
//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;
//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;
}
}
boolean r = super.add(ad);
if (size() > mWindowSize){
removeRange(0, size() - mWindowSize);
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);
}
}
}
}
++mOverlapCounter;
return r;
}
public boolean isNextWindowReady(){
if((size() > mWindowSize / 2) && mOverlapCounter > mOverlapSize){
mOverlapCounter = 1;
if(!isEmpty()){
if(((getYongest().ts - getOldest().ts) > mWindowSize) && mOverlapCounter > mOverlapSize){
mOverlapCounter = 0;
return true;
return true;
}
}
return false;
}
public static AccelerometerWindowBuffer getNewInstance(AccelerometerWindowBuffer buffer, int size, int overlap) {
double sampleRate = ((buffer.getYongest().ts - buffer.getOldest().ts) / buffer.size());
//if current size is smaller then wanted size, start at 0 and provide smaller list
int start = 0;
if ((buffer.getYongest().ts - buffer.getOldest().ts) > size) {
start = (int) Math.round(buffer.size() - (size / sampleRate));
}
// start should not be negative, this can happen due to rounding errors.
start = start < 0 ? 0 : start;
List<AccelerometerData> syncList;
synchronized (buffer) {
syncList = buffer.subList(start, buffer.size());
}
return new AccelerometerWindowBuffer(syncList, overlap);
}
public static AccelerometerWindowBuffer getNewInstance(AccelerometerWindowBuffer buffer) {
return new AccelerometerWindowBuffer(buffer, buffer.getOverlapSize());
}
public AccelerometerData getYongest() {
return get(size() - 1);
}
@@ -71,4 +128,6 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
public int getOverlapSize(){
return mOverlapSize;
}
public int getWindowSize(){return mWindowSize; }
}

Some files were not shown because too many files have changed in this diff Show More