diff --git a/README.md b/README.md
index fbf9a75..e6ec27b 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,45 @@
-#
-This app allows one to use their smartphone as a computer mouse. The movement of the smartphone is measured over an accelerometer and is then transmitted over Bluetooth to a computer.
+
+
+
+
SmartMouse is an app that aims to make a smartphone usable as a normal computer mouse. It does estimate its position by processing data from the integrated accelerometer and gyroscope with a custom sensor-fusion algorithm. This data is then transmitted to a host computer over the Bluetooth HID Profile. The mouse buttons are recreated using the Touchscreen.
+
+
+---
+
+## State
+Currently, this app is still in early development. The position estimation is currently not always correct, which leads to different artifacts and wrong movements of the cursor. Because of that, this app is not yet ready for practical use.
+
+## Features
+This app is defined by the following three core features:
+
+- **Real Position**: The real position of the device is estimated like a normal mouse using the device's internal sensors.
+- **Serverless Connection**: To connect to a target device, the Bluetooth HID Profile is used. Thus, **no** special software or server is required on the target device.
+- **Powerful Interface**: The UI of this app is designed to make it usable for everyone. However, powerful customization of the algorithm and interface is still possible over the advanced settings.
## Installation
-To install this app, you can download an APK from the [release section](https://github.com/VirtCode/SmartMouse/releases). This debug APK can then be installed on an Android device. Sometimes, sideloading apps must be enabled in the device settings.
+To install this app, you can download a built debug APK from the [release section](https://github.com/VirtCode/SmartMouse/releases). This APK can easily be installed on your Android device by just opening it. In some cases, sideloading must be enabled in the device settings in to install it.
+
+After installation, the app should be self-explanatory. First, add your computer to the devices on the "Connect" page. Then connect to it and open the "Mouse" page. Now you can use your device just like a normal mouse, by moving it on a surface and using the touchscreen to click and scroll.
+
+## Processing Algorithm
+This app uses a custom sensor-fusion algorithm to estimate the position of the device that is developed specifically for mouse-like movements.
+
+*This algorithm will be documented here in the future.*
+
+## Debugging
+**The following steps and procedures are only intended for debugging and are NOT required for normal usage.**
+
+For general debugging or just to navigate the source code conveniently, clone this repository and open the Gradle project in Android Studio or a similar IDE.
+
+To debug the algorithm, another method should be used. This app includes special debugging features. Sensor and processing data can be transmitted in real-time over the network and stored as CSV files on a computer. These files may be analyzed more precisely with data analysis software. Please note that a major delay in the normal transmission via Bluetooth may occur when the debugging transmission is also active.
-Please note that this app only supports Android version 9 or higher.
+Follow these steps to set up a debugging environment:
-## Debug Environment
-To debug this app or to navigate its source code conveniently, you may clone this repository and open the Gradle project in Android Studio or another IDE.
+1. Download [SensorServer](https://github.com/VirtCode/SensorServer) on your computer, move it to a directory you know, and run it in a terminal like described on its GitHub page.
+2. Configure debugging in your SmartMouse app on your smartphone by enabling advanced settings and enabling "Debug Transmission" in the debugging category. Change the hostname to the local IP of your computer. Restart the app for the settings to take effect.
+3. Now use the app like normal. When you enter the mouse interface, a transmission is automatically started. This transmission is ended when you leave the mouse interface. The ID of this transmission can be seen in the output of the SensorServer.
-## Note
-This project is part of a matura paper.
\ No newline at end of file
+Now you are ready to analyze transmissions which are stored as CSV in your folder by SensorServer.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1e6d06b..da6ac99 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
+
+
@@ -12,7 +14,7 @@
android:allowBackup="true"
android:icon="@mipmap/icon"
android:label="@string/app_name"
- android:supportsRtl="true"
+ android:supportsRtl="false"
android:theme="@style/Theme.SmartphoneMouse">
{}, new Parameters(PreferenceManager.getDefaultSharedPreferences(context)));
+ fetchSensor();
+ }
+
+ /**
+ * Fetches the sensor from the system.
+ */
+ private void fetchSensor() {
+ manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ accelerometer = manager.getDefaultSensor(MovementHandler.SENSOR_TYPE_ACCELEROMETER);
+ gyroscope = manager.getDefaultSensor(MovementHandler.SENSOR_TYPE_GYROSCOPE);
+ }
+
+ /**
+ * Registers itself as a listener.
+ */
+ private void register() {
+ if (registered) return;
+ manager.registerListener(this, accelerometer, MovementHandler.SAMPLING_RATE);
+ manager.registerListener(this, gyroscope, MovementHandler.SAMPLING_RATE);
+
+ registered = true;
+ }
+
+ /**
+ * Unregisters itself as a listener.
+ */
+ private void unregister() {
+ if (!registered) return;
+ manager.unregisterListener(this, accelerometer);
+ manager.unregisterListener(this, gyroscope);
+
+ registered = false;
+ }
+
+ /**
+ * Starts the measuring process.
+ *
+ * @param doneListener listener that is executed once the process has finished
+ */
+ public void calibrate(Calibration.StateListener doneListener) {
+ calibration.setListener((state) -> {
+ if (state == Calibration.STATE_END) unregister();
+
+ doneListener.update(state);
+ });
+
+ begun = false;
+ calibration.startCalibration();
+
+ register();
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.sensor.getType() == MovementHandler.SENSOR_TYPE_ACCELEROMETER) {
+
+ if (!begun) {
+ begun = true;
+ firstTime = event.timestamp;
+ }
+
+
+ float time = (event.timestamp - firstTime) * NANO_FULL_FACTOR;
+ Vec3f acceleration = new Vec3f(event.values[0], event.values[1], event.values[2]);
+
+ calibration.data(time, acceleration, gyroSample);
+
+ } else if (event.sensor.getType() == MovementHandler.SENSOR_TYPE_GYROSCOPE) {
+ this.gyroSample = new Vec3f(event.values[0], event.values[1], event.values[2]);
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/customization/DefaultSettings.java b/app/src/main/java/ch/virt/smartphonemouse/customization/DefaultSettings.java
index 146ccca..32efa5e 100644
--- a/app/src/main/java/ch/virt/smartphonemouse/customization/DefaultSettings.java
+++ b/app/src/main/java/ch/virt/smartphonemouse/customization/DefaultSettings.java
@@ -1,6 +1,7 @@
package ch.virt.smartphonemouse.customization;
import android.content.SharedPreferences;
+import ch.virt.smartphonemouse.mouse.Parameters;
/**
* This class is used for restoring the settings to their factory defaults.
@@ -27,15 +28,24 @@ public static void set(SharedPreferences preferences) {
edit.putBoolean("populated", true);
edit.putBoolean("showUsage", true);
- edit.putBoolean("debugEnabled", false);
+ edit.putBoolean("advanced", false);
edit.apply();
+ defaultDebug(preferences);
defaultInterface(preferences);
defaultMovement(preferences);
defaultCommunication(preferences);
}
+ private static void defaultDebug(SharedPreferences preferences) {
+ preferences.edit()
+ .putBoolean("debugEnabled", false)
+ .putString("debugHost", "undefined")
+ .putInt("debugPort", 55555)
+ .apply();
+ }
+
/**
* Writes the default interface settings
*
@@ -54,11 +64,11 @@ private static void defaultInterface(SharedPreferences preferences) {
edit.putFloat("interfaceVisualsIntensity", 0.5f);
edit.putBoolean("interfaceVibrationsEnable", true);
- edit.putInt("interfaceVibrationsButtonIntensity", 100);
+ edit.putInt("interfaceVibrationsButtonIntensity", 50);
edit.putInt("interfaceVibrationsButtonLength", 30);
- edit.putInt("interfaceVibrationsScrollIntensity", 50);
+ edit.putInt("interfaceVibrationsScrollIntensity", 25);
edit.putInt("interfaceVibrationsScrollLength", 20);
- edit.putInt("interfaceVibrationsSpecialIntensity", 100);
+ edit.putInt("interfaceVibrationsSpecialIntensity", 50);
edit.putInt("interfaceVibrationsSpecialLength", 50);
edit.putFloat("interfaceLayoutHeight", 0.3f);
@@ -73,33 +83,7 @@ private static void defaultInterface(SharedPreferences preferences) {
* @param preferences preferences to write in
*/
private static void defaultMovement(SharedPreferences preferences) {
- SharedPreferences.Editor edit = preferences.edit();
-
- edit.putFloat("movementSensitivity", 13);
-
- edit.putBoolean("movementScaleEnable", true);
-
- edit.putBoolean("movementSamplingCalibrated", false);
- edit.putInt("movementSamplingRealRate", 200);
-
- edit.putInt("movementLowPassOrder", 1);
- edit.putFloat("movementLowPassCutoff", 0.05f);
-
- edit.putFloat("movementFreezerFreezingThreshold", 0.1f);
- edit.putFloat("movementFreezerUnfreezingThreshold", 0.04f);
- edit.putInt("movementFreezerUnfreezingSamples", 10);
-
- edit.putFloat("movementNoiseThreshold", 0.04f);
- edit.putInt("movementNoiseResetSamples", 20);
-
- edit.putInt("movementCacheDurationMinimal", 5);
- edit.putInt("movementCacheDurationMaximal", 10);
- edit.putFloat("movementCacheReleaseThreshold", 0.05f);
-
- edit.putInt("movementScalePower", 2);
- edit.putFloat("movementScaleSplit", 0.1f);
-
- edit.apply();
+ new Parameters(preferences).reset();
}
/**
diff --git a/app/src/main/java/ch/virt/smartphonemouse/customization/SamplingRateCalibrator.java b/app/src/main/java/ch/virt/smartphonemouse/customization/SamplingRateCalibrator.java
deleted file mode 100644
index df75a03..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/customization/SamplingRateCalibrator.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package ch.virt.smartphonemouse.customization;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-
-import androidx.preference.PreferenceManager;
-
-import java.util.Timer;
-import java.util.TimerTask;
-
-import ch.virt.smartphonemouse.mouse.MovementHandler;
-
-/**
- * This class is used to measure and save the sampling rate of the inbuilt accelerometer.
- */
-public class SamplingRateCalibrator implements SensorEventListener {
-
- private static final int TEST_LENGTH = 5000;
- private static final float NANO_FULL_FACTOR = 1e-9f;
-
- private SensorManager manager;
- private Sensor sensor;
- private boolean registered;
-
- private boolean running;
- private long lastTime;
- private long delays;
- private int amount;
-
- private final Context context;
-
- /**
- * Creates the calibrator.
- *
- * @param context context to use
- */
- public SamplingRateCalibrator(Context context) {
- this.context = context;
-
- fetchSensor();
- }
-
- /**
- * Fetches the sensor from the system.
- */
- private void fetchSensor() {
- manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
- sensor = manager.getDefaultSensor(MovementHandler.SENSOR_TYPE);
- }
-
- /**
- * Registers itself as a listener.
- */
- private void register() {
- if (registered) return;
- manager.registerListener(this, sensor, MovementHandler.SAMPLING_RATE);
-
- registered = true;
- }
-
- /**
- * Unregisters itself as a listener.
- */
- private void unregister() {
- if (!registered) return;
- manager.unregisterListener(this, sensor);
-
- registered = false;
- }
-
- /**
- * Starts the measuring process.
- *
- * @param doneListener listener that is executed once the process has finished
- */
- public void calibrate(DoneListener doneListener) {
- register();
- prepareTest();
- running = true;
-
- new Timer().schedule(new TimerTask() {
- @Override
- public void run() {
- running = false;
- unregister();
-
- int rate = finishTest();
-
- doneListener.done(rate);
- }
- }, TEST_LENGTH);
-
- }
-
- /**
- * Initializes the variables for the process.
- */
- private void prepareTest() {
- lastTime = 0;
- delays = 0;
- amount = 0;
- }
-
- /**
- * Finishes the measuring process by processing the results and saving it into the preferences.
- */
- private int finishTest() {
- long averageDelay = delays / amount;
- float averageDelaySecond = averageDelay * NANO_FULL_FACTOR;
-
- int samplesPerSecond = Math.round(1f / averageDelaySecond);
-
- SharedPreferences.Editor edit = PreferenceManager.getDefaultSharedPreferences(context).edit();
- edit.putBoolean("movementSamplingCalibrated", true);
- edit.putInt("movementSamplingRealRate", samplesPerSecond);
- edit.apply();
-
- return samplesPerSecond;
- }
-
- /**
- * Returns how long the measuring process approximately will go.
- *
- * @return length of the process in milliseconds
- */
- public int getTestLength() {
- return TEST_LENGTH;
- }
-
- @Override
- public void onSensorChanged(SensorEvent event) {
- if (!running) return;
- if (lastTime == 0) {
- lastTime = event.timestamp;
- return;
- }
-
- long delay = event.timestamp - lastTime;
-
- amount++;
- delays += delay;
-
- lastTime = event.timestamp;
- }
-
- @Override
- public void onAccuracyChanged(Sensor sensor, int accuracy) {
- }
-
- /**
- * This interface is a listener used for when the measuring process is done.
- */
- public interface DoneListener {
-
- /**
- * Called when the process is done.
- *
- * @param samplingRate sampling rate that was measured
- */
- void done(int samplingRate);
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/Calibration.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/Calibration.java
new file mode 100644
index 0000000..ce80f8c
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/Calibration.java
@@ -0,0 +1,130 @@
+package ch.virt.smartphonemouse.mouse;
+
+import android.util.Log;
+import ch.virt.smartphonemouse.mouse.components.WindowAverage;
+import ch.virt.smartphonemouse.mouse.math.Vec3f;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Calibration {
+
+ private static final String TAG = "Calibration";
+
+ public static final int STATE_START = 0;
+ public static final int STATE_SAMPLING = 1;
+ public static final int STATE_NOISE = 3;
+ public static final int STATE_END = 4;
+
+
+ int state = STATE_START;
+ private StateListener listener;
+
+ boolean started = false;
+ float startTime = 0;
+
+ private int samples;
+
+ private List accelerationNoise;
+ private List rotationNoise;
+ private WindowAverage gravityAverage;
+ private WindowAverage noiseAverage;
+
+ private final Parameters params;
+
+ private float durationSampling;
+ private float durationNoise;
+
+ public Calibration(StateListener listener, Parameters params) {
+ this.listener = listener;
+ this.params = params;
+ }
+
+ public void setListener(StateListener listener) {
+ this.listener = listener;
+ }
+
+ public void startCalibration() {
+ Log.d(TAG, "Starting Sampling Rate Calibration");
+ samples = 0;
+ this.durationSampling = params.getCalibrationSamplingTime();
+
+ updateState(STATE_SAMPLING);
+ }
+
+ private void startNoise() {
+ Log.d(TAG, "Starting Noise Level Calibration");
+
+ float samplingRate = samples / durationSampling;
+ params.calibrateSamplingRate(samplingRate);
+
+ accelerationNoise = new ArrayList<>();
+ rotationNoise = new ArrayList<>();
+
+ gravityAverage = new WindowAverage(params.getLengthWindowGravity());
+ noiseAverage = new WindowAverage(params.getLengthWindowNoise());
+
+ this.durationNoise = params.getCalibrationNoiseTime();
+
+ updateState(STATE_NOISE);
+ }
+
+ private void endCalibration() {
+ Log.d(TAG, "Ending Calibration");
+
+ params.calibrateNoiseLevels(accelerationNoise, rotationNoise);
+ params.setCalibrated(true);
+ updateState(STATE_END);
+ }
+
+ public void data(float time, Vec3f acceleration, Vec3f angularVelocity) {
+ if (!started) {
+ startTime = time;
+ started = true;
+ }
+
+ if (state == STATE_SAMPLING) {
+
+ if (time - startTime > durationSampling) {
+ startNoise();
+ }
+
+ samples++;
+
+
+ } else if (state == STATE_NOISE) {
+
+ if (time - startTime > durationNoise) {
+ endCalibration();
+ }
+
+ float acc = acceleration.xy().abs();
+
+ // Remove gravity or rather lower frequencies
+ float gravity = gravityAverage.avg(acc);
+ acc -= gravity;
+ acc = Math.abs(acc);
+
+ // Remove noise
+ acc = noiseAverage.avg(acc);
+
+ // Calculate the rotation activation
+ float rot = Math.abs(angularVelocity.z);
+
+ accelerationNoise.add(acc);
+ rotationNoise.add(rot);
+ }
+ }
+
+ private void updateState(int state) {
+ this.state = state;
+ started = false;
+
+ listener.update(state);
+ }
+
+ public interface StateListener {
+ void update(int state);
+ }
+
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/MovementHandler.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/MovementHandler.java
index ef13718..564b2f5 100644
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/MovementHandler.java
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/MovementHandler.java
@@ -5,8 +5,10 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
-
import androidx.preference.PreferenceManager;
+import ch.virt.smartphonemouse.mouse.math.Vec2f;
+import ch.virt.smartphonemouse.mouse.math.Vec3f;
+import ch.virt.smartphonemouse.transmission.DebugTransmitter;
/**
* This class handles and calculates the movement of the mouse
@@ -15,19 +17,24 @@ public class MovementHandler implements SensorEventListener {
private static final float NANO_FULL_FACTOR = 1e-9f;
- public static final int SENSOR_TYPE = Sensor.TYPE_ACCELEROMETER;
+ public static final int SENSOR_TYPE_ACCELEROMETER = Sensor.TYPE_ACCELEROMETER;
+ public static final int SENSOR_TYPE_GYROSCOPE = Sensor.TYPE_GYROSCOPE;
public static final int SAMPLING_RATE = SensorManager.SENSOR_DELAY_FASTEST;
private SensorManager manager;
- private Sensor sensor;
-
- private Context context;
- private MouseInputs inputs;
+ private Sensor accelerometer;
+ private Sensor gyroscope;
private boolean registered;
- private long lastSample = 0;
- private Pipeline xLine, yLine;
+
+ private Vec3f gyroSample = new Vec3f(); // TODO: Make this a buffer to accommodate for vastly different sampling rates
+ private long lastTime = 0;
+ private long firstTime = 0;
+ private Processing processing;
+
+ private final Context context;
+ private final MouseInputs inputs;
/**
* Creates a movement handler.
@@ -40,17 +47,13 @@ public MovementHandler(Context context, MouseInputs inputs) {
this.inputs = inputs;
fetchSensor();
- create();
}
/**
* Creates the signal processing pipelines.
*/
- public void create() {
- int sampleRate = PreferenceManager.getDefaultSharedPreferences(context).getInt("communicationTransmissionRate", 200);
-
- xLine = new Pipeline(sampleRate, new PipelineConfig(PreferenceManager.getDefaultSharedPreferences(context)));
- yLine = new Pipeline(sampleRate, new PipelineConfig(PreferenceManager.getDefaultSharedPreferences(context)));
+ public void create(DebugTransmitter debug) {
+ processing = new Processing(debug, new Parameters(PreferenceManager.getDefaultSharedPreferences(context)));
}
/**
@@ -58,7 +61,8 @@ public void create() {
*/
private void fetchSensor() {
manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
- sensor = manager.getDefaultSensor(SENSOR_TYPE);
+ accelerometer = manager.getDefaultSensor(SENSOR_TYPE_ACCELEROMETER);
+ gyroscope = manager.getDefaultSensor(SENSOR_TYPE_GYROSCOPE);
}
/**
@@ -66,9 +70,11 @@ private void fetchSensor() {
*/
public void register() {
if (registered) return;
- manager.registerListener(this, sensor, SAMPLING_RATE);
+ manager.registerListener(this, accelerometer, SAMPLING_RATE);
+ manager.registerListener(this, gyroscope, SAMPLING_RATE);
- lastSample = 0;
+ lastTime = 0;
+ firstTime = 0;
registered = true;
}
@@ -77,7 +83,8 @@ public void register() {
*/
public void unregister() {
if (!registered) return;
- manager.unregisterListener(this, sensor);
+ manager.unregisterListener(this, accelerometer);
+ manager.unregisterListener(this, gyroscope);
registered = false;
}
@@ -85,18 +92,29 @@ public void unregister() {
@Override
public void onSensorChanged(SensorEvent event) {
if (!registered) return; // Ignore Samples when the listener is not registered
- if (lastSample == 0) { // Ignore First sample, because there is no delta
- lastSample = event.timestamp;
- return;
- }
- float delta = (event.timestamp - lastSample) * NANO_FULL_FACTOR; // Delta in Seconds
+ if (event.sensor.getType() == SENSOR_TYPE_ACCELEROMETER) {
+
+ if (firstTime == 0) { // Ignore First sample, because there is no delta
+ lastTime = event.timestamp;
+ firstTime = event.timestamp;
+ return;
+ }
+
+ float delta = (event.timestamp - lastTime) * NANO_FULL_FACTOR;
+ float time = (event.timestamp - firstTime) * NANO_FULL_FACTOR;
+ Vec3f acceleration = new Vec3f(event.values[0], event.values[1], event.values[2]);
- // Calculate and Submit values
- inputs.changeXPosition(xLine.nextForDistance(delta, event.values[0]));
- inputs.changeYPosition(-yLine.nextForDistance(delta, event.values[1]));
+ Vec2f distance = processing.next(time, delta, acceleration, gyroSample);
+ inputs.changeXPosition(distance.x);
+ inputs.changeYPosition(-distance.y);
- lastSample = event.timestamp;
+ lastTime = event.timestamp;
+
+ } else if (event.sensor.getType() == SENSOR_TYPE_GYROSCOPE) {
+ // Here we assume that the samples arrive in chronological order (which is crucial anyway), so we will always have the latest sample in this variable
+ this.gyroSample = new Vec3f(event.values[0], event.values[1], event.values[2]);
+ }
}
@Override
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/Parameters.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/Parameters.java
new file mode 100644
index 0000000..e676ed5
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/Parameters.java
@@ -0,0 +1,143 @@
+package ch.virt.smartphonemouse.mouse;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.util.List;
+
+public class Parameters {
+
+ private static final String TAG = "Parameters";
+
+ private static final float CALIBRATION_SAMPLING = 5;
+ private static final float CALIBRATION_NOISE = 10;
+
+ private static final float NOISE_RATIO_ACCELERATION = 1f;
+ private static final float NOISE_RATIO_ROTATION = 1f;
+ private static final float NOISE_FACTOR_ACCELERATION = 1.2f;
+ private static final float NOISE_FACTOR_ROTATION = 1.2f;
+
+ private static final float DURATION_THRESHOLD = 0.1f;
+ private static final float DURATION_WINDOW_GRAVITY = 0.02f;
+ private static final float DURATION_WINDOW_NOISE = 0.01f;
+ private static final float DURATION_GRAVITY = 2f;
+
+ private static final float SENSITIVITY = 15000f;
+
+ // Disable this part of the processing because it doesn't work as it should for some reason
+ private static final boolean ENABLE_GRAVITY_ROTATION = false;
+
+ private final SharedPreferences prefs;
+
+ public Parameters(SharedPreferences preferences) {
+ this.prefs = preferences;
+ }
+
+ public void reset() {
+ SharedPreferences.Editor edit = prefs.edit();
+
+ edit.putBoolean("movementCalibrated", false);
+
+ edit.putFloat("movementSensitivity", SENSITIVITY);
+
+ edit.putFloat("movementCalibrationSampling", CALIBRATION_SAMPLING);
+ edit.putFloat("movementCalibrationNoise", CALIBRATION_NOISE);
+
+ edit.putFloat("movementNoiseRatioAcceleration", NOISE_RATIO_ACCELERATION);
+ edit.putFloat("movementNoiseRatioRotation", NOISE_RATIO_ROTATION);
+ edit.putFloat("movementNoiseFactorAcceleration", NOISE_FACTOR_ACCELERATION);
+ edit.putFloat("movementNoiseFactorRotation", NOISE_FACTOR_ROTATION);
+
+ edit.putFloat("movementDurationThreshold", DURATION_THRESHOLD);
+ edit.putFloat("movementDurationWindowGravity", DURATION_WINDOW_GRAVITY);
+ edit.putFloat("movementDurationWindowNoise", DURATION_WINDOW_NOISE);
+ edit.putFloat("movementDurationGravity", DURATION_GRAVITY);
+
+ edit.putBoolean("movementEnableGravityRotation", ENABLE_GRAVITY_ROTATION);
+
+ edit.apply();
+ }
+
+ public void setCalibrated(boolean calibrated) {
+ prefs.edit().putBoolean("movementCalibrated", calibrated).apply();
+ }
+
+ public boolean isCalibrated() {
+ return prefs.getBoolean("movementCalibrated", false);
+ }
+
+ public float getCalibrationNoiseTime() {
+ return prefs.getFloat("movementCalibrationNoise", CALIBRATION_NOISE);
+ }
+
+ public float getCalibrationSamplingTime() {
+ return prefs.getFloat("movementCalibrationSampling", CALIBRATION_SAMPLING);
+ }
+
+ public void calibrateSamplingRate(float samplingRate) {
+ SharedPreferences.Editor edit = prefs.edit();
+
+ edit.putFloat("movementSampling", samplingRate);
+
+ edit.apply();
+ }
+
+ public void calibrateNoiseLevels(List accelerationNoise, List rotationNoise) {
+
+ // Sort arrays and get at ratio (same as removing top X% and getting the largest)
+ accelerationNoise.sort(Float::compareTo);
+ rotationNoise.sort(Float::compareTo);
+
+ float accelerationSample = accelerationNoise.get((int) ((accelerationNoise.size() - 1) * prefs.getFloat("movementNoiseRatioAcceleration", NOISE_RATIO_ACCELERATION)));
+ float rotationSample = rotationNoise.get((int) ((rotationNoise.size() - 1) * prefs.getFloat("movementNoiseRatioRotation", NOISE_RATIO_ROTATION)));
+
+ // Multiply factors
+ accelerationSample *= prefs.getFloat("movementNoiseFactorAcceleration", NOISE_FACTOR_ACCELERATION);
+ rotationSample *= prefs.getFloat("movementNoiseFactorRotation", NOISE_FACTOR_ROTATION);
+
+ // Persist
+ SharedPreferences.Editor edit = prefs.edit();
+
+ edit.putFloat("movementThresholdAcceleration", accelerationSample);
+ edit.putFloat("movementThresholdRotation", rotationSample);
+
+ edit.apply();
+ }
+
+ public int getLengthWindowGravity() {
+ return Math.round(prefs.getFloat("movementDurationWindowGravity", DURATION_WINDOW_GRAVITY)
+ * prefs.getFloat("movementSampling", 500));
+ }
+
+ public int getLengthWindowNoise() {
+ return Math.round(prefs.getFloat("movementDurationWindowNoise", DURATION_WINDOW_NOISE)
+ * prefs.getFloat("movementSampling", 500));
+ }
+
+ public int getLengthThreshold() {
+ return Math.round(prefs.getFloat("movementDurationThreshold", DURATION_THRESHOLD)
+ * prefs.getFloat("movementSampling", 500));
+ }
+
+ public int getLengthGravity() {
+ return Math.round(prefs.getFloat("movementDurationGravity", DURATION_GRAVITY)
+ * prefs.getFloat("movementSampling", 500));
+ }
+
+ public float getSensitivity() {
+ return prefs.getFloat("movementSensitivity", SENSITIVITY);
+ }
+
+ public float getThresholdAcceleration() {
+ return prefs.getFloat("movementThresholdAcceleration", 0.03f);
+ }
+
+ public float getThresholdRotation() {
+ return prefs.getFloat("movementThresholdRotation", 0.01f);
+ }
+
+ public boolean getEnableGravityRotation() {
+ return prefs.getBoolean("movementEnableGravityRotation", ENABLE_GRAVITY_ROTATION);
+ }
+
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/Pipeline.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/Pipeline.java
deleted file mode 100644
index b687f03..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/Pipeline.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package ch.virt.smartphonemouse.mouse;
-
-import ch.virt.smartphonemouse.mouse.elements.Freezer;
-import ch.virt.smartphonemouse.mouse.elements.Integration;
-import ch.virt.smartphonemouse.mouse.elements.LowPassFilter;
-import ch.virt.smartphonemouse.mouse.elements.NoiseCancelling;
-import ch.virt.smartphonemouse.mouse.elements.PersistentIntegration;
-import ch.virt.smartphonemouse.mouse.elements.Scaler;
-import ch.virt.smartphonemouse.mouse.elements.Sensitivity;
-import ch.virt.smartphonemouse.mouse.elements.SignCache;
-
-/**
- * This class handles all calculation to get from the raw sensor data to the distance output.
- * In order to do the calculation it uses various different components.
- */
-public class Pipeline {
-
- private int sampleRate;
- private PipelineConfig config;
-
- private LowPassFilter lowPass;
- private Freezer freezer;
- private NoiseCancelling noise;
- private PersistentIntegration velocityIntegration;
- private Integration distanceIntegration;
- private SignCache cache;
- private Scaler scaler;
- private Sensitivity sensitivity;
-
- private boolean debugging;
- private float[] debuggingValues;
-
- /**
- * Creates a signal processing pipeline.
- *
- * @param sampleRate sample rate at which the new samples will be provided
- * @param config configuration for all different components of this pipeline
- */
- public Pipeline(int sampleRate, PipelineConfig config) {
- this.config = config;
- this.sampleRate = sampleRate;
- create();
- }
-
- /**
- * Initializes all components.
- */
- private void create() {
-
- velocityIntegration = new PersistentIntegration();
- distanceIntegration = new Integration();
-
- lowPass = new LowPassFilter(config.getLowPassOrder(), sampleRate, config.getLowPassCutoff());
-
- freezer = new Freezer(config.getFreezerFreezingThreshold(), config.getFreezerUnfreezingThreshold(), config.getFreezerUnfreezingSamples());
-
- noise = new NoiseCancelling(config.getNoiseCancellingThreshold(), config.getNoiseFinalSamples(), velocityIntegration);
-
- cache = new SignCache(config.getCacheMinimalDuration(), config.getCacheMaximalDuration(), config.getCacheReleasingThreshold(), velocityIntegration, distanceIntegration);
-
- scaler = new Scaler(config.isScalerEnabled(), config.getScalerPower(), config.getScalerSplit());
-
- sensitivity = new Sensitivity(config.getSensitivityFactor());
-
- debuggingValues = new float[12];
- }
-
- /**
- * Calculates the current distance delta from a new acceleration sample
- *
- * @param delta time passed since the last sample (in seconds)
- * @param unfiltered new acceleration sample from the accelerometer
- * @return new distance delta
- */
- public float nextForDistance(float delta, float unfiltered) {
- int i = 0;
- if (debugging) debuggingValues[i++] = unfiltered;
-
- float subtract = (float) lowPass.filter(unfiltered); // Low pass for subtraction base
- if (debugging) debuggingValues[i++] = subtract;
- subtract = freezer.next(subtract, unfiltered); // Freeze if necessary
- if (debugging) debuggingValues[i++] = subtract;
-
- float acceleration = unfiltered - subtract; // Subtract the Low Passed and frozen
- if (debugging) debuggingValues[i++] = acceleration;
- acceleration = noise.cancel(acceleration); // Cancel the acceleration if too low
- if (debugging) debuggingValues[i++] = acceleration;
-
- float velocity = velocityIntegration.integrate(delta, acceleration); // Integrate velocity
- if (debugging) debuggingValues[i++] = velocity;
- velocity = cache.velocity(velocity, acceleration); // Do first caching action
- if (debugging) debuggingValues[i++] = velocity;
- velocity = noise.velocity(velocity);
- if (debugging) debuggingValues[i++] = velocity;
- velocity = scaler.scale(velocity); // Scale velocity
- if (debugging) debuggingValues[i++] = velocity;
-
- float distance = distanceIntegration.integrate(delta, velocity); // Integrate distance
- if (debugging) debuggingValues[i++] = distance;
- distance = cache.distance(distance, delta); // Do second caching action
- if (debugging) debuggingValues[i++] = distance;
- distance = sensitivity.scale(distance); // Scale distance to its sensitivity
- if (debugging) debuggingValues[i++] = distance;
-
- return distance;
- }
-
- /**
- * Enables debugging for this pipeline.
- * This will place the values in between the acceleration and distance into an array for every sample.
- */
- public void enableDebugging() {
- debugging = true;
- }
-
- /**
- * Returns all values in between the acceleration and distance in an array.
- * Make sure to enable debugging to get content in this array.
- *
- * @return values in between
- */
- public float[] getDebuggingValues() {
- return debuggingValues;
- }
-
- /**
- * Resets the pipeline to its initial state.
- */
- public void reset() {
- lowPass.reset();
- freezer.reset();
- noise.reset();
- velocityIntegration.reset();
- cache.reset();
- distanceIntegration.reset();
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/PipelineConfig.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/PipelineConfig.java
deleted file mode 100644
index 3f614b4..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/PipelineConfig.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package ch.virt.smartphonemouse.mouse;
-
-import android.content.SharedPreferences;
-
-/**
- * This class loads and contains all configuration data needed to configure a single pipeline.
- */
-public class PipelineConfig {
-
- // Acceleration Low Pass
- private int lowPassOrder;
- private float lowPassCutoff;
-
- // Acceleration Freezer
- private float freezerFreezingThreshold;
- private float freezerUnfreezingThreshold;
- private int freezerUnfreezingSamples;
-
- // Noise Cancelling
- private float noiseCancellingThreshold;
- private int noiseFinalSamples;
-
- // Sign Cache
- private int cacheMinimalDuration;
- private int cacheMaximalDuration;
- private float cacheReleasingThreshold;
-
- // Scaler
- private boolean scalerEnabled;
- private int scalerPower;
- private float scalerSplit;
-
- // Sensitivity
- private float sensitivityFactor;
-
- /**
- * Loads all config entries into this class.
- *
- * @param preferences preferences to load from.
- */
- public PipelineConfig(SharedPreferences preferences) {
- lowPassOrder = preferences.getInt("movementLowPassOrder", 1);
- lowPassCutoff = preferences.getFloat("movementLowPassCutoff", 0.1f);
-
- freezerFreezingThreshold = preferences.getFloat("movementFreezerFreezingThreshold", 0.1f);
- freezerUnfreezingThreshold = preferences.getFloat("movementFreezerUnfreezingThreshold", 0.04f);
- freezerUnfreezingSamples = preferences.getInt("movementFreezerUnfreezingSamples", 10);
-
- noiseCancellingThreshold = preferences.getFloat("movementNoiseThreshold", 0.04f);
- noiseFinalSamples = preferences.getInt("movementNoiseResetSamples", 20);
-
- cacheMinimalDuration = preferences.getInt("movementCacheDurationMinimal", 5);
- cacheMaximalDuration = preferences.getInt("movementCacheDurationMaximal", 10);
- cacheReleasingThreshold = preferences.getFloat("movementCacheReleaseThreshold", 0.05f);
-
- scalerEnabled = preferences.getBoolean("movementScaleEnable", true);
- scalerPower = preferences.getInt("movementScalePower", 2);
- scalerSplit = preferences.getFloat("movementScaleSplit", 0.1f);
-
- sensitivityFactor = preferences.getFloat("movementSensitivity", 13);
- }
-
- public int getLowPassOrder() {
- return lowPassOrder;
- }
-
- public float getLowPassCutoff() {
- return lowPassCutoff;
- }
-
- public float getFreezerFreezingThreshold() {
- return freezerFreezingThreshold;
- }
-
- public float getFreezerUnfreezingThreshold() {
- return freezerUnfreezingThreshold;
- }
-
- public int getFreezerUnfreezingSamples() {
- return freezerUnfreezingSamples;
- }
-
- public float getNoiseCancellingThreshold() {
- return noiseCancellingThreshold;
- }
-
- public int getNoiseFinalSamples() {
- return noiseFinalSamples;
- }
-
- public int getCacheMinimalDuration() {
- return cacheMinimalDuration;
- }
-
- public int getCacheMaximalDuration() {
- return cacheMaximalDuration;
- }
-
- public float getCacheReleasingThreshold() {
- return cacheReleasingThreshold;
- }
-
- public int getScalerPower() {
- return scalerPower;
- }
-
- public float getScalerSplit() {
- return scalerSplit;
- }
-
- public boolean isScalerEnabled() {
- return scalerEnabled;
- }
-
- public float getSensitivityFactor() {
- return sensitivityFactor;
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/Processing.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/Processing.java
new file mode 100644
index 0000000..46a6ad5
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/Processing.java
@@ -0,0 +1,169 @@
+package ch.virt.smartphonemouse.mouse;
+
+import ch.virt.smartphonemouse.mouse.components.*;
+import ch.virt.smartphonemouse.mouse.math.Vec2f;
+import ch.virt.smartphonemouse.mouse.math.Vec3f;
+import ch.virt.smartphonemouse.transmission.DebugTransmitter;
+
+public class Processing {
+
+ private Trapezoid3f rotationDeltaTrapezoid;
+
+ private WindowAverage activeGravityAverage;
+ private WindowAverage activeNoiseAverage;
+ private OrThreshold activeThreshold;
+ private boolean lastActive;
+
+ private WindowAverage3f gravityInactiveAverage;
+ private Vec3f gravityCurrent;
+
+ private Trapezoid2f distanceVelocityTrapezoid;
+ private Vec2f distanceVelocity;
+ private Trapezoid2f distanceDistanceTrapezoid;
+ private float sensitivity;
+
+
+ // Gravity Rotation is disabled by default because it currently does not work as expected
+ private boolean enableGravityRotation;
+
+
+ private final DebugTransmitter debug;
+
+ public Processing(DebugTransmitter debug, Parameters parameters) {
+ this.debug = debug;
+
+ // Create and configure components
+ rotationDeltaTrapezoid = new Trapezoid3f();
+
+ activeGravityAverage = new WindowAverage(parameters.getLengthWindowGravity());
+ activeNoiseAverage = new WindowAverage(parameters.getLengthWindowNoise());
+ activeThreshold = new OrThreshold(parameters.getLengthThreshold(), parameters.getThresholdAcceleration(), parameters.getThresholdRotation());
+
+ gravityInactiveAverage = new WindowAverage3f(parameters.getLengthGravity());
+ gravityCurrent = new Vec3f();
+
+ distanceVelocityTrapezoid = new Trapezoid2f();
+ distanceDistanceTrapezoid = new Trapezoid2f();
+ distanceVelocity = new Vec2f();
+ sensitivity = parameters.getSensitivity();
+
+ enableGravityRotation = parameters.getEnableGravityRotation();
+ }
+
+ public static void registerDebugColumns(DebugTransmitter debug) {
+ debug.registerColumn("time", Float.class);
+ debug.registerColumn("acceleration", Vec3f.class);
+ debug.registerColumn("angular-velocity", Vec3f.class);
+ debug.registerColumn("active-acc-abs", Float.class);
+ debug.registerColumn("active-acc-grav", Float.class);
+ debug.registerColumn("active-acc", Float.class);
+ debug.registerColumn("active-rot", Float.class);
+ debug.registerColumn("active", Boolean.class);
+ debug.registerColumn("gravity", Vec3f.class);
+ debug.registerColumn("acceleration-linear", Vec2f.class);
+ debug.registerColumn("velocity", Vec2f.class);
+ debug.registerColumn("distance", Vec2f.class);
+ }
+
+ public Vec2f next(float time, float delta, Vec3f acceleration, Vec3f angularVelocity) {
+ // Stage debug values
+ debug.stageFloat(time);
+ debug.stageVec3f(acceleration);
+ debug.stageVec3f(angularVelocity);
+
+ // Integrate rotation to distance, since that is used more often
+ Vec3f rotationDelta = rotationDeltaTrapezoid.trapezoid(delta, angularVelocity);
+
+ boolean active = active(acceleration, angularVelocity);
+ debug.stageBoolean(active);
+
+ Vec2f linearAcceleration = gravity(active, acceleration, rotationDelta);
+ debug.stageVec2f(linearAcceleration);
+
+ Vec2f distance = distance(delta, active, linearAcceleration, rotationDelta);
+ debug.stageVec2f(distanceVelocity); // Do this here because it did not fit into the method
+ debug.stageVec2f(distance);
+
+ // Handle active changes "globally" for optimization
+ lastActive = active;
+
+ debug.commit();
+ return distance;
+ }
+
+ public boolean active(Vec3f acceleration, Vec3f angularVelocity) {
+
+ // Calculate the acceleration activation
+ float acc = acceleration.xy().abs();
+ debug.stageFloat(acc);
+
+ // Remove gravity or rather lower frequencies
+ float gravity = activeGravityAverage.avg(acc);
+ debug.stageFloat(gravity);
+ acc -= gravity;
+ acc = Math.abs(acc);
+
+ // Remove noise
+ acc = activeNoiseAverage.avg(acc);
+ debug.stageFloat(acc);
+
+ // Calculate the rotation activation
+ float rot = Math.abs(angularVelocity.z);
+ debug.stageFloat(rot);
+
+ // Do the threshold
+ return activeThreshold.active(acc, rot);
+ }
+
+ public Vec2f gravity(boolean active, Vec3f acceleration, Vec3f rotationDelta) {
+
+ // Differentiate between the user being active or not
+ if (active) {
+
+ // Reset average for next phase
+ if (!lastActive) {
+ gravityInactiveAverage.reset();
+ }
+
+ // Rotate current gravity
+ if (enableGravityRotation) gravityCurrent.rotate(rotationDelta.copy().negative());
+
+ } else {
+ // Just calculate the average of the samples
+ gravityCurrent = gravityInactiveAverage.avg(acceleration);
+ }
+
+ debug.stageVec3f(gravityCurrent);
+
+ // Subtract the gravity
+ return acceleration.xy().subtract(gravityCurrent.xy());
+ }
+
+ public Vec2f distance(float delta, boolean active, Vec2f linearAcceleration, Vec3f rotationDelta) {
+
+ // Only calculate if it is active for optimization
+ if (active){
+
+ // Counter-rotate the velocity
+ distanceVelocity.rotate(-rotationDelta.z);
+
+ // Integrate to distance
+ distanceVelocity.add(distanceVelocityTrapezoid.trapezoid(delta, linearAcceleration));
+ return distanceDistanceTrapezoid.trapezoid(delta, distanceVelocity).multiply(sensitivity);
+
+ } else {
+
+ // Reset stuff
+ if (lastActive) {
+ distanceVelocity = new Vec2f();
+
+ // Clean the trapezoids because they contain a last value
+ distanceVelocityTrapezoid.reset();
+ distanceDistanceTrapezoid.reset();
+ }
+
+ return new Vec2f();
+ }
+ }
+
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/components/OrThreshold.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/OrThreshold.java
new file mode 100644
index 0000000..ed166d1
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/OrThreshold.java
@@ -0,0 +1,42 @@
+package ch.virt.smartphonemouse.mouse.components;
+
+/**
+ * This class represents a threshold which operates based on two values.
+ * It uses the values in an OR fashion, activating when one or both values surpass their threshold.
+ * It also takes a time to drop off, after the last active value pair.
+ */
+public class OrThreshold {
+
+ private final int dropoff;
+ private final float firstThreshold, secondThreshold;
+
+ private int lastActive;
+
+ /**
+ * @param dropoff amount of samples it takes from the last active sample to be inactive
+ * @param firstThreshold threshold the first value has to surpass
+ * @param secondThreshold threshold the second value has to surpass
+ */
+ public OrThreshold(int dropoff, float firstThreshold, float secondThreshold) {
+ this.dropoff = dropoff;
+ this.firstThreshold = firstThreshold;
+ this.secondThreshold = secondThreshold;
+
+ this.lastActive = dropoff;
+ }
+
+ /**
+ * Takes two new values and determines whether the threshold is activated
+ * @param first first value
+ * @param second second value
+ * @return whether the threshold is active
+ */
+ public boolean active(float first, float second) {
+ if (first > firstThreshold || second > secondThreshold) lastActive = 0;
+ else lastActive++;
+
+ return lastActive <= dropoff;
+ }
+
+
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/components/Trapezoid2f.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/Trapezoid2f.java
new file mode 100644
index 0000000..39e784f
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/Trapezoid2f.java
@@ -0,0 +1,31 @@
+package ch.virt.smartphonemouse.mouse.components;
+
+import ch.virt.smartphonemouse.mouse.math.Vec2f;
+
+/**
+ * This class is used to calculate a number of following trapezoids. Here with a Vec2 as a value
+ */
+public class Trapezoid2f {
+
+ private Vec2f last = new Vec2f();
+
+ /**
+ * Calculates the current trapezoid
+ * @param delta delta time to the last value
+ * @param next current value
+ * @return trapezoid
+ */
+ public Vec2f trapezoid(float delta, Vec2f next) {
+ Vec2f result = last.mean(next).multiply(delta);
+ last = next;
+ return result;
+ }
+
+ /**
+ * Resets the last value
+ */
+ public void reset() {
+ last = new Vec2f();
+ }
+
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/components/Trapezoid3f.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/Trapezoid3f.java
new file mode 100644
index 0000000..55db09f
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/Trapezoid3f.java
@@ -0,0 +1,31 @@
+package ch.virt.smartphonemouse.mouse.components;
+
+import ch.virt.smartphonemouse.mouse.math.Vec3f;
+
+/**
+ * This class is used to calculate a number of following trapezoids. Here with a Vec3 as a value
+ */
+public class Trapezoid3f {
+
+ private Vec3f last = new Vec3f();
+
+ /**
+ * Calculates the current trapezoid
+ * @param delta delta time to the last value
+ * @param next current value
+ * @return trapezoid
+ */
+ public Vec3f trapezoid(float delta, Vec3f next) {
+ Vec3f result = last.mean(next).multiply(delta);
+ last = next;
+ return result;
+ }
+
+ /**
+ * Resets the last value
+ */
+ public void reset() {
+ last = new Vec3f();
+ }
+
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/components/WindowAverage.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/WindowAverage.java
new file mode 100644
index 0000000..0093a14
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/WindowAverage.java
@@ -0,0 +1,44 @@
+package ch.virt.smartphonemouse.mouse.components;
+
+/**
+ * This class represents an average which operates in a window, remembering a given value of past samples to do the averaging with.
+ */
+public class WindowAverage {
+
+ private int index;
+ private float[] elements;
+
+ /**
+ * @param length length of the window in values
+ */
+ public WindowAverage(int length) {
+ this.elements = new float[length];
+ index = 0;
+ }
+
+ /**
+ * Takes the next sample and calculates the current average
+ * @param next next sample
+ * @return current average, including next sample
+ */
+ public float avg(float next) {
+ elements[index % elements.length] = next;
+ index++;
+
+ float total = 0f;
+ int amount = Math.min(elements.length, index);
+ for (int i = 0; i < amount; i++) {
+ total += elements[i];
+ }
+
+ return total / amount;
+ }
+
+ /**
+ * Resets the average
+ */
+ public void reset() {
+ elements = new float[elements.length];
+ index = 0;
+ }
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/components/WindowAverage3f.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/WindowAverage3f.java
new file mode 100644
index 0000000..a2e76a0
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/components/WindowAverage3f.java
@@ -0,0 +1,49 @@
+package ch.virt.smartphonemouse.mouse.components;
+
+import ch.virt.smartphonemouse.mouse.math.Vec3f;
+
+/**
+ * This class represents an average which operates in a window, remembering a given value of past samples to do the averaging with.
+ * Here with a Vec3f as a value.
+ */
+public class WindowAverage3f {
+
+ private int index;
+ private Vec3f[] elements;
+
+ /**
+ * @param length length of the window in values
+ */
+ public WindowAverage3f(int length) {
+ this.elements = new Vec3f[length];
+ index = 0;
+ }
+
+ /**
+ * Takes the next sample and calculates the current average
+ * @param next next sample
+ * @return current average, including next sample
+ */
+ public Vec3f avg(Vec3f next) {
+
+ elements[index % elements.length] = next;
+ index++;
+
+
+ Vec3f total = new Vec3f();
+ int amount = Math.min(elements.length, index);
+ for (int i = 0; i < amount; i++) {
+ total.add(elements[i]);
+ }
+
+ return total.divide(amount);
+ }
+
+ /**
+ * Resets the average
+ */
+ public void reset() {
+ elements = new Vec3f[elements.length];
+ index = 0;
+ }
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Freezer.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Freezer.java
deleted file mode 100644
index 9064404..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Freezer.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-/**
- * This class holds the freezer component of the signal processing pipeline.
- * The freezer ensures that the subtracted gravity gets occasionally frozen so that it is not affected by the movement of the smartphone.
- */
-public class Freezer {
-
- private final float freezingThreshold;
- private final float unfreezingThreshold;
- private final int unfreezingSamples;
-
- private boolean frozen;
- private int unfreezing;
- private float subtract;
-
- /**
- * Creates a freezer component.
- *
- * @param freezingThreshold how much acceleration is required for the freezer to kick in
- * @param unfreezingThreshold how less acceleration is required for the freezing process to end
- * @param unfreezingSamples how many samples in a row must be below said threshold
- */
- public Freezer(float freezingThreshold, float unfreezingThreshold, int unfreezingSamples) {
- this.freezingThreshold = freezingThreshold;
- this.unfreezingThreshold = unfreezingThreshold;
- this.unfreezingSamples = unfreezingSamples;
- }
-
- /**
- * Calculates the next sample
- *
- * @param lowPassed acceleration after the low pass filter (gravity)
- * @param acceleration unfiltered acceleration
- * @return new gravity value, that may be frozen
- */
- public float next(float lowPassed, float acceleration) {
- if (frozen) {
-
- if (Math.abs(acceleration - lowPassed) < unfreezingThreshold)
- unfreezing++; // Count up if it may be unfrozen
- else unfreezing = 0;
-
- if (unfreezing == unfreezingSamples) { // Unfreeze if enough samples in a row
- frozen = false;
- subtract = lowPassed;
- }
-
- } else {
- subtract = lowPassed;
-
- if (Math.abs(acceleration - subtract) > freezingThreshold) { // Freeze if below threshold
- frozen = true;
- unfreezing = 0;
- }
- }
-
- return subtract;
- }
-
- /**
- * Resets the current state of the freezer.
- */
- public void reset(){
- frozen = false;
- unfreezing = 0;
- subtract = 0;
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Integration.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Integration.java
deleted file mode 100644
index a688524..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Integration.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-/**
- * This class handles basic integration that is not persistent and can is used for the signal processing.
- */
-public class Integration {
-
- private float last;
-
- /**
- * Integrates with the newly given sample.
- *
- * @param delta time delta since the last sample
- * @param value new sample
- * @return integrated delta
- */
- public float integrate(float delta, float value) {
- return ((last + (last = value)) / 2) * delta;
- }
-
- /**
- * Resets previously stored variables.
- */
- public void reset() {
- last = 0;
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/LowPassFilter.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/LowPassFilter.java
deleted file mode 100644
index 0f4cb91..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/LowPassFilter.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-import uk.me.berndporr.iirj.Bessel;
-
-/**
- * This class wraps a bessel filter from the iirj library as a low pass filter and provides it for the signal processing.
- */
-public class LowPassFilter extends Bessel {
-
- /**
- * Creates a low pass filter.
- *
- * @param order order of the filter
- * @param sampleRate sample rate with which samples are expected
- * @param cutoff cutoff frequency of the filter
- */
- public LowPassFilter(int order, int sampleRate, float cutoff) {
- super();
- lowPass(order, sampleRate, cutoff);
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/NoiseCancelling.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/NoiseCancelling.java
deleted file mode 100644
index 6b359c3..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/NoiseCancelling.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-/**
- * This class holds the noise cancelling component for the signal processing.
- * The noise cancelling removes all amplitudes below a certain value. It also does support clearing the velocity if there has been only noise for a certain amount of time.
- */
-public class NoiseCancelling {
-
- private final float cancellingThreshold;
- private final int finalCancellingSamples;
-
- private final Integration velocityIntegration;
-
- private int noise;
-
- /**
- * Creates a noise cancelling component.
- *
- * @param cancellingThreshold all signal below this threshold is regarded as noise
- * @param finalCancellingSamples how many samples in a row must be noise in order to reset the velocity
- * @param velocityIntegration integration of the velocity that may be reset
- */
- public NoiseCancelling(float cancellingThreshold, int finalCancellingSamples, Integration velocityIntegration) {
- this.cancellingThreshold = cancellingThreshold;
- this.finalCancellingSamples = finalCancellingSamples;
- this.velocityIntegration = velocityIntegration;
- }
-
- /**
- * Process the values that may be cancelled, presumably the acceleration.
- *
- * @param value current acceleration value
- * @return possibly canceled value
- */
- public float cancel(float value) {
-
- if (Math.abs(value) <= cancellingThreshold) { // Below Threshold
- noise++;
- return 0;
- } else noise = 0;
-
- return value;
- }
-
- /**
- * Process the velocity to potentially reset it.
- *
- * @param value current velocity value
- * @return possibly reset velocity
- */
- public float velocity(float value) {
- if (noise > finalCancellingSamples) {
- velocityIntegration.reset();
- return 0; // Cancel if much noise
- }
- return value;
- }
-
- /**
- * Resets the noise cancelling.
- */
- public void reset(){
- noise = 0;
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/PersistentIntegration.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/PersistentIntegration.java
deleted file mode 100644
index 2f024cb..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/PersistentIntegration.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-/**
- * This class handles integration that is persistent (just like normal integration)
- */
-public class PersistentIntegration extends Integration {
-
- private float whole;
-
- @Override
- public float integrate(float delta, float value) {
-
- whole += super.integrate(delta, value);
- return whole;
-
- }
-
- @Override
- public void reset() {
- super.reset();
-
- whole = 0;
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Scaler.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Scaler.java
deleted file mode 100644
index 871f192..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Scaler.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-/**
- * This class is a component for the signal processing that scales the signal.
- * The signal gets scaled so that low values get even lower, and high values even higher.
- */
-public class Scaler {
-
- private final boolean enabled;
- private final int power;
- private final float split;
-
- /**
- * Creates a scaler.
- *
- * @param enabled whether the scaler is enabled
- * @param power the power of the function that is used for scaling
- * @param split values over this value get higher, values lower than it get lower
- */
- public Scaler(boolean enabled, int power, float split) {
- this.enabled = enabled;
- this.power = power;
- this.split = split;
- }
-
- /**
- * Scales a value with the function.
- *
- * @param value value that is scaled
- * @return the scaled value
- */
- public float scale(float value) {
- if (!enabled) return value;
-
- return (float) (Math.pow(Math.abs(value), power) * (split / Math.pow(split, power))) * (value > 0 ? 1 : -1);
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Sensitivity.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Sensitivity.java
deleted file mode 100644
index a28b8aa..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/Sensitivity.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-/**
- * This component of the signal processing is used to scale the signal by a fixed factor.
- * The factor of this scaling is made up of a base and a power, because (i think) humans do only feel exponential increase.
- */
-public class Sensitivity {
-
- private final int base;
- private final float power;
-
- private float factor;
-
- /**
- * Creates a sensitivity component.
- *
- * @param base base of the factor
- * @param power power of the factor
- */
- public Sensitivity(int base, float power) {
- this.base = base;
- this.power = power;
-
- factor = (float) Math.pow(base, power); // Factor can be pre calculated since the base and power do not change
- }
-
- /**
- * Creates a sensitivity component with the base of two.
- *
- * @param power power of the factor
- */
- public Sensitivity(float power) {
- this(2, power); // Two, so one unit is double
- }
-
- /**
- * Scales the value by the factor.
- *
- * @param value value to be scaled
- * @return scaled value
- */
- public float scale(float value) {
- return value * factor;
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/SignCache.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/SignCache.java
deleted file mode 100644
index 66d16f1..0000000
--- a/app/src/main/java/ch/virt/smartphonemouse/mouse/elements/SignCache.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package ch.virt.smartphonemouse.mouse.elements;
-
-/**
- * The sign cache component caches the velocity after the sign switches. If quickly after that, the acceleration rises drastically, the cached values get used again.
- * This component is used to remove invalid movements that occur after a sign change.
- */
-public class SignCache {
-
- private final int minimalDuration;
- private final int maximalDuration;
- private final float releasingThreshold;
-
- private final Integration velocityIntegration;
- private final Integration distanceIntegration;
-
- private boolean caching;
- private boolean latelyChanged;
- private float lastValue;
- private int duration;
- private float cachedDistance;
-
- /**
- * Creates a sign cache.
- *
- * @param minimalDuration minimal duration the signal gets cached
- * @param maximalDuration maximal duration the signal gets cached
- * @param releasingThreshold how high the acceleration has to be in order for the values in the cache to get used again
- * @param velocityIntegration integration for the velocity that may be cleared
- * @param distanceIntegration integration for the distance so the distance loss can be compensated
- */
- public SignCache(int minimalDuration, int maximalDuration, float releasingThreshold, Integration velocityIntegration, Integration distanceIntegration) {
- this.minimalDuration = minimalDuration;
- this.maximalDuration = maximalDuration;
- this.releasingThreshold = releasingThreshold;
- this.velocityIntegration = velocityIntegration;
- this.distanceIntegration = distanceIntegration;
- }
-
- /**
- * Potentially caches the velocity.
- *
- * @param value current velocity value
- * @param acceleration current acceleration value
- * @return possibly canceled velocity
- */
- public float velocity(float value, float acceleration) {
- if (value != 0 && lastValue != 0 && (lastValue > 0 != value > 0)) { // If sign switched start caching
- caching = true;
- duration = 0;
- cachedDistance = 0;
- }
-
- lastValue = value;
-
- if (caching) {
-
- if (duration >= minimalDuration) {
-
- if (Math.abs(acceleration) >= releasingThreshold) { // If releasing soon enough, just return to normal
- caching = false;
- latelyChanged = true;
- return value;
- }
-
- if (duration >= maximalDuration) { // If not released, reset velocity
- velocityIntegration.reset();
- caching = false;
- return 0;
- }
- }
-
- duration++;
- return 0;
- }
-
- return value;
- }
-
- /**
- * Potentially caches the moved distance.
- *
- * @param value current distance value
- * @param delta time delta since the last value
- * @return possibly cached distance
- */
- public float distance(float value, float delta) {
- if (caching) { // If caching, add values up
-
- cachedDistance += distanceIntegration.integrate(delta, lastValue);
-
- return 0;
- } else if (latelyChanged) { // If changed to non caching, release distance Cache
- latelyChanged = false;
-
- return value + cachedDistance;
-
- } else return value;
- }
-
- /**
- * Resets the cache to its defaults.
- */
- public void reset() {
- caching = false;
- latelyChanged = false;
- lastValue = 0;
- duration = 0;
- cachedDistance = 0;
- }
-}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/math/Vec2f.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/math/Vec2f.java
new file mode 100644
index 0000000..7342e38
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/math/Vec2f.java
@@ -0,0 +1,92 @@
+package ch.virt.smartphonemouse.mouse.math;
+
+/**
+ * This class represents a vector consisting of two floats.
+ */
+public class Vec2f {
+ public float x, y;
+
+ public Vec2f() {}
+ public Vec2f(float x, float y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Creates a copy of the vector
+ */
+ public Vec2f copy() {
+ return new Vec2f(x, y);
+ }
+
+ /**
+ * Returns the absolute value, aka. length of the vector
+ */
+ public float abs() {
+ return (float) Math.sqrt(x*x + y*y);
+ }
+
+ /**
+ * Adds another vector to this one
+ */
+ public Vec2f add(Vec2f other) {
+ x += other.x;
+ y += other.y;
+
+ return this;
+ }
+
+ /**
+ * Adds another vector to this one
+ */
+ public Vec2f subtract(Vec2f other) {
+ x -= other.x;
+ y -= other.y;
+
+ return this;
+ }
+
+ /**
+ * Multiplies or scales this vector by an amount
+ */
+ public Vec2f multiply(float factor) {
+ x *= factor;
+ y *= factor;
+
+ return this;
+ }
+
+ /**
+ * Divides this vector by a given amount
+ */
+ public Vec2f divide(float divisor) {
+ x /= divisor;
+ y /= divisor;
+
+ return this;
+ }
+
+ /**
+ * Calculates the average between this and a given other vector. Not affecting this instance.
+ */
+ public Vec2f mean(Vec2f second) {
+ return this.copy().add(second).divide(2);
+ }
+
+ /**
+ * Rotates this vector by a given rotation
+ * @param rotation rotation in radians
+ */
+ public Vec2f rotate(float rotation) {
+ float c = (float) Math.cos(rotation);
+ float s = (float) Math.sin(rotation);
+
+ float newX = c * x + (-s) * y;
+ float newY = s * x + c * y;
+
+ this.x = newX;
+ this.y = newY;
+
+ return this;
+ }
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/mouse/math/Vec3f.java b/app/src/main/java/ch/virt/smartphonemouse/mouse/math/Vec3f.java
new file mode 100644
index 0000000..7029457
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/mouse/math/Vec3f.java
@@ -0,0 +1,134 @@
+package ch.virt.smartphonemouse.mouse.math;
+
+/**
+ * This class represents a three-dimensional vector, based on floats
+ */
+public class Vec3f {
+ public float x, y, z;
+
+ public Vec3f() {}
+ public Vec3f(float x, float y, float z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ /**
+ * Returns the x and y axes as a 2d vector
+ */
+ public Vec2f xy() {
+ return new Vec2f(x, y);
+ }
+
+ /**
+ * Returns the y and z axes as a 2d vector
+ */
+ public Vec2f yz() {
+ return new Vec2f(y, z);
+ }
+
+ /**
+ * Returns the x and z axes as a 2d vector
+ */
+ public Vec2f xz() {
+ return new Vec2f(x, z);
+ }
+
+ /**
+ * Returns the absolute value or length of the vector
+ */
+ public float abs() {
+ return (float) Math.sqrt(x*x + y*y + z*z);
+ }
+
+ /**
+ * Creates a copy of this vector
+ */
+ public Vec3f copy() {
+ return new Vec3f(x, y, z);
+ }
+
+ /**
+ * Adds another vector to this one
+ */
+ public Vec3f add(Vec3f other) {
+ x += other.x;
+ y += other.y;
+ z += other.z;
+
+ return this;
+ }
+
+ /**
+ * Adds another vector to this one
+ */
+ public Vec3f subtract(Vec3f other) {
+ x -= other.x;
+ y -= other.y;
+ z -= other.z;
+
+ return this;
+ }
+
+ /**
+ * Multiplies or scales this vector by an amount
+ */
+ public Vec3f multiply(float factor) {
+ x *= factor;
+ y *= factor;
+ z *= factor;
+
+ return this;
+ }
+
+ /**
+ * Divides this vector by a given amount
+ */
+ public Vec3f divide(float divisor) {
+ x /= divisor;
+ y /= divisor;
+ z /= divisor;
+
+ return this;
+ }
+
+ /**
+ * Negates this vector
+ */
+ public Vec3f negative() {
+ return this.multiply(-1f);
+ }
+
+ /**
+ * Calculates the average between this and a given other vector. Not affecting this instance.
+ */
+ public Vec3f mean(Vec3f second) {
+ return this.copy().add(second).divide(2);
+ }
+
+ /**
+ * Rotates this vector by all three axis by using euler angles in a xyz fashion
+ * @param rotation vector containing the rotation for each axis as radians
+ */
+ public Vec3f rotate(Vec3f rotation) {
+
+ // Calculate sines and cosines that are used (for optimization)
+ float sa = (float) Math.sin(rotation.x);
+ float ca = (float) Math.cos(rotation.x);
+ float sb = (float) Math.sin(rotation.y);
+ float cb = (float) Math.cos(rotation.y);
+ float sc = (float) Math.sin(rotation.z);
+ float cc = (float) Math.cos(rotation.z);
+
+ // Apply the rotation (matrix used: xyz)
+ float newX = cb * cc * x + cb * (-sc) * y + sb * z;
+ float newY = ((-sa) * (-sb) * cb + ca * sc) * x + ((-sa) * (-sb) * (-sc) + ca * cc) * y + (-sa) * cb * z;
+ float newZ = (ca * (-sb) * cc + sa * sc) * x + (ca * (-sb) * (-sc) + sa * cc) * y + ca * cb * z;
+
+ this.x = newX;
+ this.y = newY;
+ this.z = newZ;
+
+ return this;
+ }
+}
diff --git a/app/src/main/java/ch/virt/smartphonemouse/transmission/DebugTransmitter.java b/app/src/main/java/ch/virt/smartphonemouse/transmission/DebugTransmitter.java
new file mode 100644
index 0000000..ab8180d
--- /dev/null
+++ b/app/src/main/java/ch/virt/smartphonemouse/transmission/DebugTransmitter.java
@@ -0,0 +1,396 @@
+package ch.virt.smartphonemouse.transmission;
+
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.util.Log;
+import ch.virt.smartphonemouse.mouse.Processing;
+import ch.virt.smartphonemouse.mouse.math.Vec2f;
+import ch.virt.smartphonemouse.mouse.math.Vec3f;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+
+/**
+ * This class handles transmission to a SensorServer. It is used with the following workflow:
+ *
First, the transmission should be connected to a SensorServer (see DebugTransmitter#connect()). During this process, the transmitter logs in and transmits its columns.
+ *
After a successful connection, the transmitter may start a transmission (see DebugTransmitter#startTransmission()).
+ *
If a transmission is open, the transmitter can operate. To transmit data, a client has to stage data in the order, he has registered the columns earlier. If all data for a data row is staged, data has to be committed (see DebugTransmitter#commmit()).
+ *
After a transmission, the transmission can be ended (see DebugTransmitter#endTransmission()), and the connection closed if required (see DebugTransmitter#disconnect())