adding conductorssensor using the mbientlab metaboard r+

This commit is contained in:
toni
2018-05-18 22:07:42 +02:00
parent 85ea37c14b
commit 38c39a045c
56 changed files with 2963 additions and 6 deletions

View File

@@ -11,8 +11,8 @@ android {
minSdkVersion 24
targetSdkVersion 26
//sdk 2 | product version 3 | build num 2 | multi-apk 2
versionCode 260130300
versionName "0.1.3.2"
versionCode 260130400
versionName "0.1.3.3"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

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 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildToolsVersion '27.0.3'
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
api 'com.mbientlab:metawear:3.4.0'
implementation 'com.android.support:preference-v7:26.1.0'
implementation 'com.android.support:preference-v14:26.1.0'
compile 'com.github.wendykierp:JTransforms:3.1'
}

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,30 @@
<?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"/>
<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,155 @@
package de.tonifetzer.conductorssensor;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.os.Bundle;
import android.support.v7.widget.PopupMenu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.Toast;
import android.widget.ToggleButton;
import java.util.Stack;
import de.tonifetzer.conductorssensor.estimation.Estimator;
import de.tonifetzer.conductorssensor.sensor.ConnectFragment;
import de.tonifetzer.conductorssensor.settings.SettingsFragment;
public class MainActivity extends FragmentActivity implements PopupMenu.OnMenuItemClickListener, ToggleButton.OnCheckedChangeListener{
ConnectFragment mConnectFragment;
Stack<String> mFragmentStack;
ToggleButton mStartStopToggle;
Estimator mEstimator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// register listener for start stop btn
mStartStopToggle= (ToggleButton) findViewById(R.id.startBtn);
mStartStopToggle.setOnCheckedChangeListener(this);
// ensures the connection to the bt sensor board
mConnectFragment = new ConnectFragment();
mFragmentStack = new Stack<>();
}
@Override
public void onBackPressed() {
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
if(!mFragmentStack.empty()){
// hide current top, and pop it
String top = mFragmentStack.pop();
t.hide(getSupportFragmentManager().findFragmentByTag(top));
if(!mFragmentStack.empty()){
//open new top of stack
top = mFragmentStack.peek();
t.show(getSupportFragmentManager().findFragmentByTag(top));
} else{
// if its now empty activate our mainView
findViewById(R.id.bpmContent).setVisibility(View.VISIBLE);
findViewById(R.id.backBtn).setVisibility(View.INVISIBLE);
}
t.commit();
} else {
super.onBackPressed();
}
}
public void onBackPressed(View v){
onBackPressed();
}
public void openFragment(Fragment aFragment, String tag){
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
if(!mFragmentStack.empty()){
if(mFragmentStack.peek().equals(tag)){
// if fragment is already open, do nothing
return;
} else {
//hide to current top
String top = mFragmentStack.peek();
t.hide(getSupportFragmentManager().findFragmentByTag(top));
}
} else {
//disable mainView items
findViewById(R.id.bpmContent).setVisibility(View.INVISIBLE);
findViewById(R.id.backBtn).setVisibility(View.VISIBLE);
}
if (getSupportFragmentManager().findFragmentByTag(tag) == null) {
t.add(R.id.mainContent, aFragment, tag);
} else {
t.show(getSupportFragmentManager().findFragmentByTag(tag));
}
mFragmentStack.push(tag);
t.commit();
}
public void showPopup(View v) {
PopupMenu popup = new PopupMenu(this, v);
MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.settings, popup.getMenu());
popup.setOnMenuItemClickListener(this);
popup.show();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_bluetooth:
openFragment(mConnectFragment, "Connect");
break;
case R.id.action_settings:
openFragment(new SettingsFragment(), "Settings");
break;
}
return false;
}
@Override
public void finish() {
//super.finish();
moveTaskToBack(true);
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
if (isChecked) {
if(mConnectFragment.getSensorBoard() != null && mConnectFragment.getSensorBoard().isConnected()){
mConnectFragment.getSensorBoard().startAccelerometer();
//todo: estimator classe mit start und stop funktion. board bei start durchreichen.
} else {
Toast.makeText(this, "Please connect a sensor!", Toast.LENGTH_SHORT).show();
mStartStopToggle.setChecked(false);
}
} else {
if(mConnectFragment.getSensorBoard() != null){
mConnectFragment.getSensorBoard().stopAccelerometer();
}
}
}
}

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

@@ -0,0 +1,105 @@
package de.tonifetzer.conductorssensor.estimation;
import java.util.Arrays;
public class AccelerometerInterpolator {
private double[] mX;
private double[] mY;
private double[] mZ;
private long[] mTsInterp;
public AccelerometerInterpolator(AccelerometerWindowBuffer ab, double sampleRate_ms){
long size = (ab.getYongest().ts - (ab.getOldest().ts - (long) sampleRate_ms)) / (long) sampleRate_ms;
mTsInterp = new long[(int)size];
int j = 0;
for(long i = ab.getOldest().ts; i <= ab.getYongest().ts; i += sampleRate_ms){
mTsInterp[j++] = i;
}
mX = interpLinear(ab.getTs(), ab.getX(), mTsInterp);
mY = interpLinear(ab.getTs(), ab.getY(), mTsInterp);
mZ = interpLinear(ab.getTs(), ab.getZ(), mTsInterp);
}
public long[] getTs(){
return mTsInterp;
}
public double[] getX(){
return mX;
}
public double[] getY(){
return mY;
}
public double[] getZ(){
return mZ;
}
private static double[] interpLinear(double[] x, double[] y, double[] xi) throws IllegalArgumentException {
if (x.length != y.length) {
throw new IllegalArgumentException("X and Y must be the same length");
}
if (x.length == 1) {
throw new IllegalArgumentException("X must contain more than one value");
}
double[] dx = new double[x.length - 1];
double[] dy = new double[x.length - 1];
double[] slope = new double[x.length - 1];
double[] intercept = new double[x.length - 1];
// Calculate the line equation (i.e. slope and intercept) between each point
for (int i = 0; i < x.length - 1; i++) {
dx[i] = x[i + 1] - x[i];
if (dx[i] == 0) {
throw new IllegalArgumentException("X must be montotonic. A duplicate " + "x-value was found");
}
if (dx[i] < 0) {
throw new IllegalArgumentException("X must be sorted");
}
dy[i] = y[i + 1] - y[i];
slope[i] = dy[i] / dx[i];
intercept[i] = y[i] - x[i] * slope[i];
}
// Perform the interpolation here
double[] yi = new double[xi.length];
for (int i = 0; i < xi.length; i++) {
if ((xi[i] > x[x.length - 1]) || (xi[i] < x[0])) {
yi[i] = Double.NaN;
}
else {
int loc = Arrays.binarySearch(x, xi[i]);
if (loc < -1) {
loc = -loc - 2;
yi[i] = slope[loc] * xi[i] + intercept[loc];
}
else {
yi[i] = y[loc];
}
}
}
return yi;
}
private static double[] interpLinear(long[] x, double[] y, long[] xi) throws IllegalArgumentException {
double[] xd = new double[x.length];
for (int i = 0; i < x.length; i++) {
xd[i] = x[i];
}
double[] xid = new double[xi.length];
for (int i = 0; i < xi.length; i++) {
xid[i] = xi[i];
}
return interpLinear(xd, y, xid);
}
}

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

@@ -0,0 +1,213 @@
package de.tonifetzer.conductorssensor.estimation;
import de.tonifetzer.conductorssensor.utilities.MovingFilter;
import de.tonifetzer.conductorssensor.utilities.SimpleKalman;
import de.tonifetzer.conductorssensor.utilities.Utils;
import java.util.LinkedList;
/**
* Created by toni on 17/12/17.
*/
public class BpmEstimator {
private AccelerometerWindowBuffer mBuffer;
private double mSampleRate_ms;
private LinkedList<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;
public BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
mBuffer = windowBuffer;
mSampleRate_ms = sampleRate_ms;
mBpmHistory_X = new LinkedList<>();
mBpmHistory_Y = new LinkedList<>();
mBpmHistory_Z = new LinkedList<>();
mBpmHistory = new LinkedList<>();
mResetCounter = 0;
mResetLimit_ms = resetAfter_ms;
//TODO: this is to easy. since the new dyn. windowsize produces smaller update times, we need to consider something, that
//TODO: holds more values, if they are similar, an resets the history if not.
mMvg = new MovingFilter(2);
//mKalman = new SimpleKalman();
}
//TODO: we use the buffer from outside.. this buffer is continuously updated.. not good!
public double estimate(AccelerometerWindowBuffer fixedWindow){
double sampleRate = mSampleRate_ms;
if(sampleRate <= 0){
sampleRate = Math.round(Utils.mean(Utils.diff(fixedWindow.getTs())));
}
if(sampleRate == 0){
int breakhere = 0;
}
AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate);
//are we conducting?
//just look at the newest 512 samples
//List<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);
Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false);
Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false);
Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false);
mBpmHistory_X.add(pX.getBPM(sampleRate));
mBpmHistory_Y.add(pY.getBPM(sampleRate));
mBpmHistory_Z.add(pZ.getBPM(sampleRate));
double estimatedBPM = getBestBpmEstimation(pX, pY, pZ);
if(estimatedBPM != -1){
//moving avg (lohnt dann, wenn wir viele daten haben)
mMvg.add(estimatedBPM);
mBpmHistory.add(mMvg.getAverage());
//mBpmHistory.add(estimatedBPM);
//moving median (lohnt nur bei konstantem tempo, da wir nur ein tempo damit gut halten können.)
//mMvg.add(estimatedBPM);
//mBpmHistory.add(mMvg.getMedian());
//kalman filter (lohnt dann, wenn wir konstantes tempo haben, mit startangabe!)
//standard last element
//mBpmHistory.add(estimatedBPM);
//mResetCounter = 0;
}
else {
int resetAfter = (int) Math.round(mResetLimit_ms / (mBuffer.getOverlapSize()));
if(++mResetCounter > resetAfter){
mBpmHistory.clear();
//TODO: send signal to clear.
//mBuffer.clear();
mMvg.clear();
mResetCounter = 0;
}
return -1;
}
//last element
return mBpmHistory.getLast();
}
public double getMeanBpm(){
return Utils.mean(mBpmHistory);
}
public double getMedianBPM(){
return Utils.median(mBpmHistory);
}
private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException {
int cntNumAxis = 0;
double sumCorr = 0; //to prevent division by zero
double sumRms = 0;
double sumNumInter = 0;
double corrMeanX = 0, corrRmsX = 0;
int corrNumInterX = 0;
if(peaksX.hasPeaks()){
corrMeanX = Utils.geometricMean(peaksX.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsX = Utils.rms(peaksX.getPeaksValueWithoutZeroIdx());
corrNumInterX = Utils.intersectionNumber(peaksX.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanX;
sumRms += corrRmsX;
sumNumInter += corrNumInterX;
}
double corrMeanY = 0, corrRmsY = 0;
int corrNumInterY = 0;
if(peaksY.hasPeaks()){
corrMeanY = Utils.geometricMean(peaksY.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsY = Utils.rms(peaksY.getPeaksValueWithoutZeroIdx());
corrNumInterY = Utils.intersectionNumber(peaksY.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanY;
sumRms += corrRmsY;
sumNumInter += corrNumInterY;
}
double corrMeanZ = 0, corrRmsZ = 0;
int corrNumInterZ = 0;
if(peaksZ.hasPeaks()){
corrMeanZ = Utils.geometricMean(peaksZ.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsZ = Utils.rms(peaksZ.getPeaksValueWithoutZeroIdx());
corrNumInterZ = Utils.intersectionNumber(peaksZ.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanZ;
sumRms += corrRmsZ;
sumNumInter += corrNumInterZ;
}
//no peaks, reject
if(cntNumAxis == 0){
//throw new IllegalArgumentException("All Peaks are empty! -> Reject Estimation");
return -1;
}
/*
System.out.println("RMS-X: " + corrRmsX);
System.out.println("GEO-X: " + corrMeanX);
System.out.println("INTER-X: " + corrNumInterX);
System.out.println("RMS-Y: " + corrRmsY);
System.out.println("GEO-Y: " + corrMeanY);
System.out.println("INTER-Y: " + corrNumInterY);
System.out.println("RMS-Z: " + corrRmsZ);
System.out.println("GEO-Z: " + corrMeanZ);
System.out.println("INTER-Z: " + corrNumInterZ);
*/
//values to low, reject
//TODO: this is a pretty simple assumption. first shot!
if(corrRmsX < 0.25 && corrRmsY < 0.25 && corrRmsZ < 0.25){
return -1;
}
double quantityX = ((corrMeanX / sumCorr) + (corrRmsX / sumRms) + (corrNumInterX / sumNumInter)) / cntNumAxis;
double quantityY = ((corrMeanY / sumCorr) + (corrRmsY / sumRms) + (corrNumInterY / sumNumInter)) / cntNumAxis;
double quantityZ = ((corrMeanZ / sumCorr) + (corrRmsZ / sumRms) + (corrNumInterZ / sumNumInter)) / cntNumAxis;
//get best axis by quantity and estimate bpm
if(quantityX > quantityY && quantityX > quantityZ){
return mBpmHistory_X.getLast();
}
else if(quantityY > quantityZ){
return mBpmHistory_Y.getLast();
}
else {
return mBpmHistory_Z.getLast();
}
}
}

View File

@@ -0,0 +1,166 @@
package de.tonifetzer.conductorssensor.estimation;
import android.hardware.Sensor;
import android.util.Log;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import de.tonifetzer.conductorssensor.sensor.SensorBoard;
import de.tonifetzer.conductorssensor.utilities.Utils;
public class Estimator implements SensorBoard.OnSensorBoardDataListener {
private SensorBoard mSensorBoard;
private AccelerometerWindowBuffer mAccelerometerWindowBuffer;
private BpmEstimator mBpmEstimator;
private Timer mTimer = new Timer();
Estimator(){
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000);
}
public void start(SensorBoard sensorBoard){
if(mSensorBoard != null){
mSensorBoard = sensorBoard;
mSensorBoard.addListener(this);
mSensorBoard.startAccelerometer();
startWorker();
} else {
Log.i("Estimator","Cant start estimator. SensorBoard is null.");
}
}
public void stop(){
if(mSensorBoard != null){
mSensorBoard.removeListener(this);
mSensorBoard.stopAccelerometer();
mAccelerometerWindowBuffer.clear();
} else {
Log.i("Estimator","Cant stop estimator. SensorBoard is null.");
}
}
@Override
public void onAccelerometerChanged(AccelerometerData data) {
mAccelerometerWindowBuffer.add(data);
//todo: save data into stream and write on disk
}
private void startWorker() {
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (mAccelerometerWindowBuffer.isNextWindowReady()) {
LinkedList<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 add(OnEstimationListener listener){mEstimationListeners.add(listener);}
public void remove(OnEstimationListener listener){mEstimationListeners.remove(listener);}
}

View File

@@ -0,0 +1,206 @@
package de.tonifetzer.conductorssensor.estimation;
import de.tonifetzer.conductorssensor.utilities.Utils;
import java.util.LinkedList;
/**
* Created by toni on 15/12/17.
*/
public class Peaks {
private LinkedList<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 Peaks(double[] data, int width, double threshold, double decayRate, boolean isRelative){
this.mData = data;
this.mPeaksIdx = new LinkedList<>();
this.mPeaksPos = new LinkedList<>();
this.mPeaksValue = new LinkedList<>();
//create the peaks
simplePeakFinder(data, width, threshold, decayRate, isRelative);
updateLists();
}
public LinkedList<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

@@ -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,268 @@
package de.tonifetzer.conductorssensor.sensor;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.mbientlab.metawear.Data;
import com.mbientlab.metawear.MetaWearBoard;
import com.mbientlab.metawear.Route;
import com.mbientlab.metawear.Subscriber;
import com.mbientlab.metawear.android.BtleService;
import com.mbientlab.metawear.builder.RouteBuilder;
import com.mbientlab.metawear.builder.RouteComponent;
import com.mbientlab.metawear.data.Acceleration;
import com.mbientlab.metawear.module.Accelerometer;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import bolts.Continuation;
import bolts.Task;
import de.tonifetzer.conductorssensor.estimation.AccelerometerData;
public class SensorBoard implements ServiceConnection {
private BtleService.LocalBinder serviceBinder;
private MetaWearBoard mMetaBoard;
private BluetoothDevice mBluetoothDevice;
private Accelerometer mAccelerometer;
private boolean mBoardConnected = false;
private Context mContext;
SensorBoard(Context context){
mContext = context;
// bind the btleService from MetaWear
mContext.bindService(new Intent(mContext, BtleService.class),
this, Context.BIND_AUTO_CREATE);
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
serviceBinder = (BtleService.LocalBinder) iBinder;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
}
protected void disconnect(){
if(!mBoardConnected){
return;
}
mMetaBoard.tearDown();
boolean isCompleted = false;
try {
isCompleted = mMetaBoard.disconnectAsync().waitForCompletion(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!isCompleted){
Toast.makeText(mContext, "Failed to disconnect. Try again.", Toast.LENGTH_SHORT).show();
Log.i("SensorBoard", "Failed to disconnect Metaboard after 5 seconds.");
return;
}
serviceBinder.removeMetaWearBoard(mBluetoothDevice);
//note: it is important to call this in main loop!
for(OnSensorBoardConnectListener listener : mConnectListeners){
listener.onSensorDisconnected();
}
mBluetoothDevice = null;
mBoardConnected = false;
Toast.makeText(mContext, "Disconnected", Toast.LENGTH_LONG).show();
}
protected void onDestroy() {
// Unbind the service when the activity is destroyed
mContext.unbindService(this);
}
protected void connect(BluetoothDevice device){
//connect with metawear
mMetaBoard = serviceBinder.getMetaWearBoard(device);
mMetaBoard.connectAsync().continueWith(new Continuation<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
.commit();
mAccelerometer.acceleration().addRouteAsync(new RouteBuilder() {
@Override
public void configure(RouteComponent source) {
source.stream(new Subscriber() {
@Override
public void apply(Data data, Object... env) {
Log.i("MainActivity", data.value(Acceleration.class).toString());
for(OnSensorBoardDataListener listener : mDataListeners){
listener.onAccelerometerChanged(new AccelerometerData(
System.currentTimeMillis(),
data.value(Acceleration.class).x(),
data.value(Acceleration.class).y(),
data.value(Acceleration.class).z()));
}
}
});
}
}).continueWith(new Continuation<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();
}
}
/**
* 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,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,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 260130301
versionName "0.1.3.2"
versionCode 260130401
versionName "0.1.3.3"
}
buildTypes {
release {

View File

@@ -1,8 +1,6 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.ArrayList;
import java.util.List;
/**
* Created by toni on 15/12/17.