Skip to content

Commit

Permalink
Merge pull request #3 from VirtCode/next
Browse files Browse the repository at this point in the history
Version v1.4.0
  • Loading branch information
VirtCode authored Mar 31, 2023
2 parents ed90463 + 5e8109e commit 2acd7bf
Show file tree
Hide file tree
Showing 79 changed files with 2,374 additions and 2,567 deletions.
48 changes: 40 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
# <img src="brand/banner.svg"/>
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.
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="brand/title-dark.svg">
<img alt="SmartMouse logo and text" src="brand/title-light.svg">
</picture>

<p>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.</p>
</div>

---

## 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.
Now you are ready to analyze transmissions which are stored as CSV in your folder by SensorServer.
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS"/>
<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.VIBRATE" />

<application
android:allowBackup="true"
android:icon="@mipmap/icon"
android:label="@string/app_name"
android:supportsRtl="true"
android:supportsRtl="false"
android:theme="@style/Theme.SmartphoneMouse">
<activity
android:name=".MainActivity"
Expand Down
47 changes: 23 additions & 24 deletions app/src/main/java/ch/virt/smartphonemouse/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
Expand All @@ -13,21 +12,16 @@
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;

import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.navigation.NavigationView;

import ch.virt.smartphonemouse.customization.DefaultSettings;
import ch.virt.smartphonemouse.mouse.MouseInputs;
import ch.virt.smartphonemouse.mouse.MovementHandler;
import ch.virt.smartphonemouse.mouse.Parameters;
import ch.virt.smartphonemouse.transmission.BluetoothHandler;
import ch.virt.smartphonemouse.ui.AboutFragment;
import ch.virt.smartphonemouse.ui.ConnectFragment;
import ch.virt.smartphonemouse.ui.DebugFragment;
import ch.virt.smartphonemouse.ui.HomeFragment;
import ch.virt.smartphonemouse.ui.MouseFragment;
import ch.virt.smartphonemouse.ui.SettingsFragment;
import ch.virt.smartphonemouse.ui.mouse.MouseCalibrateDialog;
import ch.virt.smartphonemouse.transmission.DebugTransmitter;
import ch.virt.smartphonemouse.ui.*;
import ch.virt.smartphonemouse.ui.settings.dialog.CalibrateDialog;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.navigation.NavigationView;

/**
* This class is the main activity of this app.
Expand All @@ -43,6 +37,8 @@ public class MainActivity extends AppCompatActivity implements PreferenceFragmen
private MovementHandler movement;
private MouseInputs inputs;

private DebugTransmitter debug;

private boolean mouseActive;

private boolean instanceSaved = false; // Used to avoid ui changes if the activity is not rendered.
Expand All @@ -59,10 +55,17 @@ protected void onCreate(Bundle savedInstanceState) {

loadContent();

startDebugging();

navigate(R.id.drawer_home);
drawer.setCheckedItem(R.id.drawer_home);
}

private void startDebugging() {
debug = new DebugTransmitter(PreferenceManager.getDefaultSharedPreferences(this));
debug.connect();
}


/**
* Loads the components into their variables.
Expand Down Expand Up @@ -119,8 +122,6 @@ private void checkNavItems() {
setNavItemEnable(R.id.drawer_connect, false);
setNavItemEnable(R.id.drawer_mouse, false);
}

drawer.getMenu().findItem(R.id.drawer_debug).setVisible(PreferenceManager.getDefaultSharedPreferences(this).getBoolean("debugEnabled", false));
}

/**
Expand Down Expand Up @@ -201,9 +202,9 @@ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
public boolean navigate(int entry) {
if (entry == R.id.drawer_mouse) {

if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("movementSamplingCalibrated", false)) { // Make sure that the sampling rate is calibrated
if (!new Parameters(PreferenceManager.getDefaultSharedPreferences(this)).isCalibrated()) { // Make sure that the sampling rate is calibrated

MouseCalibrateDialog dialog = new MouseCalibrateDialog();
CalibrateDialog dialog = new CalibrateDialog();
dialog.show(getSupportFragmentManager(), null);

return true;
Expand All @@ -214,7 +215,10 @@ public boolean navigate(int entry) {

mouseActive = true;

movement.create();
movement.create(debug);
debug.connect();

debug.startTransmission();
movement.register();
inputs.start();

Expand All @@ -223,6 +227,7 @@ public boolean navigate(int entry) {

if (mouseActive) {
movement.unregister();
debug.endTransmission();
inputs.stop();
mouseActive = false;
}
Expand All @@ -237,7 +242,7 @@ public boolean navigate(int entry) {

case R.id.drawer_home:

switchFragment(new HomeFragment(bluetooth), false);
switchFragment(new HomeFragment(bluetooth, debug), false);
bar.setTitle(R.string.title_home);

break;
Expand All @@ -256,12 +261,6 @@ public boolean navigate(int entry) {

break;

case R.id.drawer_debug:
switchFragment(new DebugFragment(), false);
bar.setTitle(R.string.title_debug);
bar.setVisibility(View.GONE);
break;

default:
Toast.makeText(this, "Not yet implemented!", Toast.LENGTH_SHORT).show();
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package ch.virt.smartphonemouse.customization;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import androidx.preference.PreferenceManager;
import ch.virt.smartphonemouse.mouse.Calibration;
import ch.virt.smartphonemouse.mouse.MovementHandler;
import ch.virt.smartphonemouse.mouse.Parameters;
import ch.virt.smartphonemouse.mouse.math.Vec3f;

/**
* This class is used to measure and save the sampling rate of the inbuilt accelerometer.
*/
public class CalibrationHandler implements SensorEventListener {

private static final float NANO_FULL_FACTOR = 1e-9f;

private SensorManager manager;
private Sensor accelerometer;
private Sensor gyroscope;

private boolean begun = false;
private long firstTime = 0;
private Vec3f gyroSample = new Vec3f();
private boolean registered;

private final Context context;
private final Calibration calibration;

/**
* Creates the calibrator.
*
* @param context context to use
*/
public CalibrationHandler(Context context) {
this.context = context;

calibration = new Calibration((state) -> {}, 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) {}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
*
Expand All @@ -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);
Expand All @@ -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();
}

/**
Expand Down
Loading

0 comments on commit 2acd7bf

Please sign in to comment.