Updated motion handling, updated FitnessActivity to match motion handling
This commit is contained in:
@@ -38,9 +38,6 @@ dependencies {
|
|||||||
implementation 'org.joml:joml:1.10.5'
|
implementation 'org.joml:joml:1.10.5'
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
implementation 'com.aldebaran:qisdk:1.7.5'
|
implementation 'com.aldebaran:qisdk:1.7.5'
|
||||||
|
@@ -3,21 +3,31 @@ package com.example.fitbot.exercise;
|
|||||||
public enum EMuscleGroup {
|
public enum EMuscleGroup {
|
||||||
// TODO: Implement
|
// TODO: Implement
|
||||||
|
|
||||||
TORSO(0),
|
TORSO(0, new String[]{"upper body", "torso"}),
|
||||||
ARMS(1),
|
ARMS(1, new String[]{"arms", "arm", "shoulder"}),
|
||||||
LEGS(2),
|
LEGS(2, new String[]{"Lower body", "legs", "leg"});
|
||||||
BALANCE(3);
|
|
||||||
|
|
||||||
int muscleGroupIdentifier;
|
int muscleGroupIdentifier;
|
||||||
|
String[] muscleGroupNames;
|
||||||
|
|
||||||
EMuscleGroup(int identifier) {
|
EMuscleGroup(int identifier, String[] muscleGroupNames) {
|
||||||
this.muscleGroupIdentifier = identifier;
|
this.muscleGroupIdentifier = identifier;
|
||||||
|
this.muscleGroupNames = muscleGroupNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getIdentifier() {
|
public int getIdentifier() {
|
||||||
return this.muscleGroupIdentifier;
|
return this.muscleGroupIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static EMuscleGroup parse(String name)
|
||||||
|
{
|
||||||
|
for ( EMuscleGroup muscleGroup : EMuscleGroup.values())
|
||||||
|
for ( String muscleGroupName : muscleGroup.muscleGroupNames)
|
||||||
|
if ( muscleGroupName.equalsIgnoreCase(name))
|
||||||
|
return muscleGroup;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static EMuscleGroup parse(int identifier) {
|
public static EMuscleGroup parse(int identifier) {
|
||||||
for (EMuscleGroup muscleGroup : EMuscleGroup.values()) {
|
for (EMuscleGroup muscleGroup : EMuscleGroup.values()) {
|
||||||
if (muscleGroup.getIdentifier() == identifier) {
|
if (muscleGroup.getIdentifier() == identifier) {
|
||||||
|
@@ -3,26 +3,21 @@ package com.example.fitbot.exercise;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.example.fitbot.util.path.GesturePath;
|
import com.example.fitbot.util.path.GesturePath;
|
||||||
import com.example.fitbot.util.processing.IMotionDataConsumer;
|
|
||||||
import com.example.fitbot.util.server.IWebServerHandler;
|
import com.example.fitbot.util.server.IWebServerHandler;
|
||||||
import com.example.fitbot.util.server.WebServer;
|
import com.example.fitbot.util.server.WebServer;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class Exercise implements IWebServerHandler {
|
public class Exercise {
|
||||||
|
|
||||||
private EMuscleGroup muscleGroup;
|
|
||||||
private GesturePath leftPath;
|
|
||||||
private GesturePath rightPath;
|
|
||||||
private String title;
|
|
||||||
private String description;
|
|
||||||
private float segmentsPerSecond;
|
|
||||||
|
|
||||||
// Static fields.
|
|
||||||
private static WebServer webSocket;
|
|
||||||
private static Exercise currentExercise = null;
|
|
||||||
|
|
||||||
|
public final EMuscleGroup muscleGroup;
|
||||||
|
public final GesturePath leftPath;
|
||||||
|
public final GesturePath rightPath;
|
||||||
|
public final String title;
|
||||||
|
public final String description;
|
||||||
|
public final String imageUrl;
|
||||||
|
public final String videoUrl;
|
||||||
|
public final float exerciseTimeInSeconds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the AbstractExercise class.
|
* Constructor for the AbstractExercise class.
|
||||||
@@ -32,106 +27,17 @@ public class Exercise implements IWebServerHandler {
|
|||||||
* @param rightPath The path of the right hand.
|
* @param rightPath The path of the right hand.
|
||||||
* @param title The title of the exercise.
|
* @param title The title of the exercise.
|
||||||
* @param description The description of the exercise.
|
* @param description The description of the exercise.
|
||||||
* @param segmentsPerSecond The number of segments per second.
|
* @param imageUrl The URL of the image.
|
||||||
* This determines how fast the exercise should be performed.
|
* @param videoUrl The URL of the video.
|
||||||
*/
|
*/
|
||||||
public Exercise(EMuscleGroup muscleGroup, String title, String description, GesturePath leftPath, GesturePath rightPath) {
|
public Exercise(EMuscleGroup muscleGroup, String title, String description, String imageUrl, String videoUrl, GesturePath leftPath, GesturePath rightPath, float exerciseTimeInSeconds) {
|
||||||
this.muscleGroup = muscleGroup;
|
this.muscleGroup = muscleGroup;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.leftPath = leftPath;
|
this.leftPath = leftPath;
|
||||||
this.rightPath = rightPath;
|
this.rightPath = rightPath;
|
||||||
}
|
this.imageUrl = imageUrl;
|
||||||
|
this.videoUrl = videoUrl;
|
||||||
/**
|
this.exerciseTimeInSeconds = exerciseTimeInSeconds;
|
||||||
* Start the exercise.
|
|
||||||
* This method starts a WebSocket server
|
|
||||||
*/
|
|
||||||
public final void startExercise() {
|
|
||||||
|
|
||||||
// Ensure no other exercise is active.
|
|
||||||
if (currentExercise != null && currentExercise != this) {
|
|
||||||
currentExercise.__stopExercise();
|
|
||||||
Log.i("Exercises", "Another exercise was started when another was still running.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a WebSocket server is already running, change the event handler to be this class.
|
|
||||||
if (webSocket != null && webSocket.isConnected()) {
|
|
||||||
webSocket.setEventHandler(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
webSocket = WebServer.createServer();
|
|
||||||
Objects.requireNonNull(webSocket, "WebSocket server could not be created.");
|
|
||||||
|
|
||||||
webSocket.setEventHandler(this);
|
|
||||||
currentExercise = this;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method for ending this exercise and returning the grade of the performance
|
|
||||||
* of this activity.
|
|
||||||
*/
|
|
||||||
public final double finishExercise() {
|
|
||||||
this.__stopExercise();
|
|
||||||
|
|
||||||
// TODO: Implement grade calculation
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the exercise.
|
|
||||||
* This method stops the WebSocket server.
|
|
||||||
*/
|
|
||||||
private void __stopExercise() {
|
|
||||||
if (webSocket != null && webSocket.isConnected()) {
|
|
||||||
webSocket.stop();
|
|
||||||
webSocket = null;
|
|
||||||
}
|
|
||||||
currentExercise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current exercise is the current activity.
|
|
||||||
*/
|
|
||||||
public final boolean isCurrentActivity() {
|
|
||||||
return currentExercise == this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the muscle group of the exercise.
|
|
||||||
*/
|
|
||||||
public EMuscleGroup getMuscleGroup() {
|
|
||||||
return muscleGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path of the exercise.
|
|
||||||
*/
|
|
||||||
public GesturePath[] getPath() {
|
|
||||||
return new GesturePath[]{leftPath, rightPath};
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the speed of the exercise.
|
|
||||||
*/
|
|
||||||
public double getSegmentsPerSecond() {
|
|
||||||
return segmentsPerSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(String message) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,25 +4,35 @@ import com.example.fitbot.util.path.GesturePath;
|
|||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
import org.joml.Vector3f;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
|
||||||
public class ExerciseManager {
|
public class ExerciseManager {
|
||||||
|
|
||||||
private static final String HOST_ADDRESS = "http://145.92.8.132";
|
private static final String HOST_ADDRESS = "http://145.92.8.132:443/";
|
||||||
|
|
||||||
|
// The value of these property variables must be equivalent of
|
||||||
|
// the JSON data that the database sends back.
|
||||||
|
// If this is not the case then the exercise retrieval will fail.
|
||||||
|
private static final String PROPERTY_MUSCLE_GROUP = "muscleGroup";
|
||||||
private static final String PROPERTY_DESC = "description";
|
private static final String PROPERTY_DESC = "description";
|
||||||
private static final String PROPERTY_VECTORS = "vector_data";
|
private static final String PROPERTY_IMAGE_URL = "imageUrl";
|
||||||
|
private static final String PROPERTY_VIDEO_URL = "videoUrl";
|
||||||
private static final String PROPERTY_NAME = "name";
|
private static final String PROPERTY_NAME = "name";
|
||||||
private static final String PROPERTY_MUSCLE_GROUP = "muscle_group";
|
private static final String PROPERTY_DATA = "data";
|
||||||
private static final String PROPERTY_SEGMENT_SPEED = "segment_speed";
|
private static final String PROPERTY_EXERCISE_DURATION = "exerciseDuration";
|
||||||
|
|
||||||
|
public static final int SENSOR_COUNT = 2;
|
||||||
|
|
||||||
|
private static final String[] REQUIRED_PROPERTIES = {
|
||||||
|
PROPERTY_MUSCLE_GROUP, PROPERTY_DESC, PROPERTY_IMAGE_URL,
|
||||||
|
PROPERTY_VIDEO_URL, PROPERTY_NAME, PROPERTY_DATA,
|
||||||
|
PROPERTY_EXERCISE_DURATION
|
||||||
|
};
|
||||||
|
|
||||||
private static final float DEFAULT_SEGMENT_SPEED = 1.0f;
|
private static final float DEFAULT_SEGMENT_SPEED = 1.0f;
|
||||||
|
|
||||||
@@ -36,13 +46,17 @@ public class ExerciseManager {
|
|||||||
*
|
*
|
||||||
* @return The response from the server.
|
* @return The response from the server.
|
||||||
*/
|
*/
|
||||||
private static String sendHTTP(String url, String method, String contentType, String body) {
|
public static String sendHTTP(String url, String method, String contentType, String body) {
|
||||||
try {
|
try {
|
||||||
URLConnection connection = new URL(url).openConnection();
|
URLConnection connection = new URL(url).openConnection();
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
connection.setDoInput(true);
|
||||||
connection.addRequestProperty("Content-Type", contentType);
|
connection.addRequestProperty("Content-Type", contentType);
|
||||||
connection.addRequestProperty("Request-Method", method);
|
connection.addRequestProperty("Request-Method", method);
|
||||||
connection.getOutputStream().write(body.getBytes());
|
|
||||||
connection.connect();
|
connection.connect();
|
||||||
|
// Send a body if it is present
|
||||||
|
if (body != null)
|
||||||
|
connection.getOutputStream().write(body.getBytes());
|
||||||
InputStream stream = connection.getInputStream();
|
InputStream stream = connection.getInputStream();
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
@@ -60,23 +74,41 @@ public class ExerciseManager {
|
|||||||
/**
|
/**
|
||||||
* Function for retrieving an exercise from the Raspberry Pi Database.
|
* Function for retrieving an exercise from the Raspberry Pi Database.
|
||||||
*
|
*
|
||||||
* @param uniqueIdentifier The unique identifier of the exercise
|
|
||||||
* @return The exercise, if it exists on the server. Otherwise null.
|
* @return The exercise, if it exists on the server. Otherwise null.
|
||||||
*/
|
*/
|
||||||
public static Exercise retrieveExercise(String uniqueIdentifier) {
|
public static Exercise retrieveExercise() {
|
||||||
String response = sendHTTP(
|
String response = sendHTTP(
|
||||||
HOST_ADDRESS + "/acquire", "GET", "application/json", "{\"kind\":\"" + uniqueIdentifier + "\"}"
|
HOST_ADDRESS, "POST", "application/json", null
|
||||||
);
|
);
|
||||||
// Validate the response
|
// Validate the response
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
try {
|
try {
|
||||||
JsonObject content = JsonParser.parseString(response).getAsJsonObject();
|
JsonObject content = JsonParser.parseString(response).getAsJsonObject();
|
||||||
|
|
||||||
|
// Ensure all required properties are present
|
||||||
|
for (String property : REQUIRED_PROPERTIES) {
|
||||||
|
if (!content.has(property)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path data is split into two parts, due to the left and right hand.
|
||||||
|
// If one wants to add support for more sensors, one will have to adjust the Exercise
|
||||||
|
// class to support more paths.
|
||||||
|
String[] leftRightData = content.get(PROPERTY_DATA).getAsString().split(";");
|
||||||
|
|
||||||
|
if ( leftRightData.length != SENSOR_COUNT)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new Exercise(
|
return new Exercise(
|
||||||
EMuscleGroup.parse(content.get(PROPERTY_MUSCLE_GROUP).getAsInt()),
|
EMuscleGroup.parse(content.get(PROPERTY_MUSCLE_GROUP).getAsString()),
|
||||||
content.get(PROPERTY_NAME).getAsString(),
|
content.get(PROPERTY_NAME).getAsString(),
|
||||||
content.get(PROPERTY_DESC).getAsString(),
|
content.get(PROPERTY_DESC).getAsString(),
|
||||||
gesturePathFromString(content.get(PROPERTY_VECTORS).getAsString()),
|
content.get(PROPERTY_IMAGE_URL).getAsString(),
|
||||||
gesturePathFromString(content.get(PROPERTY_SEGMENT_SPEED).getAsString())
|
content.get(PROPERTY_VIDEO_URL).getAsString(),
|
||||||
|
GesturePath.fromString(leftRightData[0]),
|
||||||
|
GesturePath.fromString(leftRightData[1]),
|
||||||
|
DEFAULT_SEGMENT_SPEED
|
||||||
);
|
);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -84,42 +116,4 @@ public class ExerciseManager {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for converting a string to a GesturePath object.
|
|
||||||
* The input string bytes will be directly converted into 3d vectors.
|
|
||||||
* Every scalar is composed of 32 bits (4 characters), meaning 96 bits per vector.
|
|
||||||
*
|
|
||||||
* Note: ASCII to Vector conversion is done in Big Endian format (most significant byte first).
|
|
||||||
*
|
|
||||||
* @param input The string to convert
|
|
||||||
* @return The GesturePath object
|
|
||||||
*/
|
|
||||||
private static GesturePath gesturePathFromString(String input) {
|
|
||||||
byte[] bytes = input.getBytes();
|
|
||||||
|
|
||||||
// Check if the input string contains a valid amount of bytes (12 bytes per vector)
|
|
||||||
if (input.length() % 12 != 0) {
|
|
||||||
throw new IllegalArgumentException("Invalid input string length");
|
|
||||||
}
|
|
||||||
GesturePath.Builder builder = new GesturePath.Builder();
|
|
||||||
|
|
||||||
float[] xyz = new float[3];
|
|
||||||
for (int i = 0; i < bytes.length; i += 12) {
|
|
||||||
for (int j = 0; j < 3; j++) {
|
|
||||||
|
|
||||||
xyz[j] = Float.intBitsToFloat(
|
|
||||||
(bytes[i + j * 4] & 0xFF) << 24 |
|
|
||||||
(bytes[i + j * 4 + 1] & 0xFF) << 16 |
|
|
||||||
(bytes[i + j * 4 + 2] & 0xFF) << 8 |
|
|
||||||
(bytes[i + j * 4 + 3] & 0xFF)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
builder.addVector(new Vector3f(
|
|
||||||
xyz[0], xyz[1], xyz[2]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package com.example.fitbot.ui.activities;
|
package com.example.fitbot.ui.activities;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.VideoView;
|
import android.widget.VideoView;
|
||||||
|
|
||||||
import com.aldebaran.qi.sdk.QiContext;
|
import com.aldebaran.qi.sdk.QiContext;
|
||||||
@@ -10,25 +9,43 @@ import com.aldebaran.qi.sdk.RobotLifecycleCallbacks;
|
|||||||
import com.aldebaran.qi.sdk.design.activity.RobotActivity;
|
import com.aldebaran.qi.sdk.design.activity.RobotActivity;
|
||||||
import com.aldebaran.qi.sdk.design.activity.conversationstatus.SpeechBarDisplayStrategy;
|
import com.aldebaran.qi.sdk.design.activity.conversationstatus.SpeechBarDisplayStrategy;
|
||||||
import com.example.fitbot.R;
|
import com.example.fitbot.R;
|
||||||
import com.example.fitbot.exercise.EMuscleGroup;
|
|
||||||
import com.example.fitbot.exercise.Exercise;
|
import com.example.fitbot.exercise.Exercise;
|
||||||
|
import com.example.fitbot.exercise.ExerciseManager;
|
||||||
import com.example.fitbot.ui.components.PersonalMotionPreviewElement;
|
import com.example.fitbot.ui.components.PersonalMotionPreviewElement;
|
||||||
import com.example.fitbot.util.ButtonNavigation;
|
import com.example.fitbot.util.ButtonNavigation;
|
||||||
import com.example.fitbot.util.FitnessCycle;
|
import com.example.fitbot.util.FitnessCycle;
|
||||||
import com.example.fitbot.util.path.GesturePath;
|
import com.example.fitbot.util.processing.InputProcessor;
|
||||||
|
|
||||||
import org.joml.Vector3f;
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks {
|
public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks {
|
||||||
|
|
||||||
PersonalMotionPreviewElement personalMotionPreviewElement;
|
// Private fields for the FitnessActivity class.
|
||||||
|
private PersonalMotionPreviewElement personalMotionPreviewElement;
|
||||||
|
private InputProcessor motionProcessor;
|
||||||
|
private Exercise currentExercise;
|
||||||
|
|
||||||
|
private QiContext qiContext;
|
||||||
|
|
||||||
|
// Some nice little messages for the user
|
||||||
|
private static final String[] EXERCISE_NOT_FOUND_MESSAGES = new String[] {
|
||||||
|
"Ik heb momenteel helaas wat moeite met het ophalen van oefeningen, sorry.",
|
||||||
|
"Het lijkt erop dat de oefeningen op een misterieus avontuur zijn. Even wachten tot ze terug zijn.",
|
||||||
|
"Ssst, de oefeningen slapen nog, probeer het later nog eens."
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE =
|
||||||
|
"Indien dit probleem zich voortzet, neem contact op met de ontwikkelaar.";
|
||||||
|
|
||||||
|
private static final float SENSOR_SAMPLE_RATE = 10.0f;
|
||||||
|
private static final int EXERCISE_COUNT = 5;
|
||||||
|
private static final float EXERCISE_SPEED_MULTIPLIER = 1.0f;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
QiSDK.register(this, this);
|
QiSDK.register(this, this);
|
||||||
|
|
||||||
setContentView(R.layout.activity_fitness);
|
setContentView(R.layout.activity_fitness);
|
||||||
|
|
||||||
// Remove the ugly ass bar on top of the view
|
// Remove the ugly ass bar on top of the view
|
||||||
@@ -36,49 +53,62 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
|
|||||||
|
|
||||||
// Find the VideoView by its ID
|
// Find the VideoView by its ID
|
||||||
VideoView videoView = findViewById(R.id.videoView);
|
VideoView videoView = findViewById(R.id.videoView);
|
||||||
|
|
||||||
FitnessCycle.playVideo(videoView, this);
|
FitnessCycle.playVideo(videoView, this);
|
||||||
|
|
||||||
ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class);
|
ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class);
|
||||||
// Implement your logic when the robot focus is gained
|
// Implement your logic when the robot focus is gained
|
||||||
|
|
||||||
GesturePath.Builder gesturePathBuilder = new GesturePath.Builder();
|
|
||||||
|
|
||||||
/* Generate a random path to test the tracking system */
|
|
||||||
for ( int i = 0; i < 40; i++)
|
|
||||||
{
|
|
||||||
gesturePathBuilder.addVector(
|
|
||||||
new Vector3f(
|
|
||||||
(float)Math.cos(Math.PI + (Math.PI / 40.0f) * i),
|
|
||||||
(float)Math.sin(Math.PI + (Math.PI / 40.0f) * i),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement);
|
|
||||||
personalMotionPreviewElement.post(() -> {
|
|
||||||
Log.i("FitnessActivity", "PersonalMotionPreviewElement.post()");
|
|
||||||
|
|
||||||
Exercise exercise = new Exercise(EMuscleGroup.ARMS, "Bicep Curls", "Oefening voor de biceps.", gesturePathBuilder.build(), gesturePathBuilder.build());
|
|
||||||
|
|
||||||
personalMotionPreviewElement.initialize(exercise);
|
|
||||||
personalMotionPreviewElement.provideQiContext(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRobotFocusGained(QiContext qiContext) {
|
public void onRobotFocusGained(QiContext qiContext) {
|
||||||
|
this.qiContext = qiContext;
|
||||||
// Find the VideoView by its ID
|
// Find the VideoView by its ID
|
||||||
// CompletableFuture.runAsync(() -> FitnessCycle.executeMovement("bicepcurl", 10, qiContext));
|
// CompletableFuture.runAsync(() -> FitnessCycle.executeMovement("bicepcurl", 10, qiContext));
|
||||||
Log.i("Motion", "qiContext provided");
|
|
||||||
|
personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement);
|
||||||
|
|
||||||
|
// Initialize the element whenever it has been added to the screen.
|
||||||
|
// This will provide the element with the appropriate dimensions for drawing
|
||||||
|
// the canvas properly.
|
||||||
|
personalMotionPreviewElement.post(() -> {
|
||||||
|
Exercise exercise = this.acquireExercise();
|
||||||
|
if ( exercise == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Acquire paths from the exercise and provide them to the motion processor
|
||||||
|
Vector3f[][] vectors = new Vector3f[][] {exercise.leftPath.getVectors(), exercise.rightPath.getVectors()};
|
||||||
|
|
||||||
|
motionProcessor = new InputProcessor(vectors, exercise.exerciseTimeInSeconds, SENSOR_SAMPLE_RATE);
|
||||||
|
|
||||||
|
personalMotionPreviewElement.provideQiContext(qiContext);
|
||||||
|
personalMotionPreviewElement.initialize(exercise, motionProcessor, EXERCISE_COUNT);
|
||||||
|
|
||||||
|
motionProcessor.startListening();
|
||||||
|
motionProcessor.setInputHandler(personalMotionPreviewElement);
|
||||||
|
});
|
||||||
personalMotionPreviewElement.provideQiContext(qiContext);
|
personalMotionPreviewElement.provideQiContext(qiContext);
|
||||||
|
|
||||||
// FitnessCycle.playVideo(qiContext, videoView, this);
|
// FitnessCycle.playVideo(qiContext, videoView, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire an exercise from the ExerciseManager.
|
||||||
|
* Whenever the retrieval failed, it will have the robot say something to the user
|
||||||
|
* to inform them about the issue.
|
||||||
|
*
|
||||||
|
* @return The acquired exercise, or null if the exercise could not be retrieved.
|
||||||
|
*/
|
||||||
|
public Exercise acquireExercise() {
|
||||||
|
Exercise exercise = ExerciseManager.retrieveExercise();
|
||||||
|
if ( exercise == null && this.qiContext != null)
|
||||||
|
{
|
||||||
|
int randomMessageIndex = (int)Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length);
|
||||||
|
FitnessCycle.say(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex], qiContext);
|
||||||
|
FitnessCycle.say(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE, qiContext);
|
||||||
|
}
|
||||||
|
return exercise;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRobotFocusLost() {
|
public void onRobotFocusLost() {
|
||||||
// Implement your logic when the robot focus is lost
|
// Implement your logic when the robot focus is lost
|
||||||
@@ -93,6 +123,8 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
|
|||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
QiSDK.unregister(this, this);
|
QiSDK.unregister(this, this);
|
||||||
this.personalMotionPreviewElement.onDestroy();
|
this.motionProcessor.stopListening();
|
||||||
|
this.motionProcessor = null;
|
||||||
|
this.personalMotionPreviewElement.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,18 +1,23 @@
|
|||||||
package com.example.fitbot.ui.components;
|
package com.example.fitbot.ui.components;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.aldebaran.qi.sdk.QiContext;
|
import com.aldebaran.qi.sdk.QiContext;
|
||||||
import com.example.fitbot.exercise.Exercise;
|
import com.example.fitbot.exercise.Exercise;
|
||||||
|
import com.example.fitbot.ui.activities.EndScreenActivity;
|
||||||
|
import com.example.fitbot.ui.activities.FitnessActivity;
|
||||||
|
import com.example.fitbot.ui.activities.MainActivity;
|
||||||
import com.example.fitbot.util.FitnessCycle;
|
import com.example.fitbot.util.FitnessCycle;
|
||||||
import com.example.fitbot.util.path.GesturePath;
|
import com.example.fitbot.util.processing.IInputHandler;
|
||||||
import com.example.fitbot.util.processing.MotionProcessor;
|
import com.example.fitbot.util.processing.InputProcessor;
|
||||||
|
|
||||||
import org.joml.Matrix4f;
|
import org.joml.Matrix4f;
|
||||||
import org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
@@ -20,61 +25,58 @@ import org.joml.Vector3f;
|
|||||||
import org.joml.Vector4f;
|
import org.joml.Vector4f;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
public class PersonalMotionPreviewElement extends View {
|
public class PersonalMotionPreviewElement extends View implements IInputHandler {
|
||||||
|
|
||||||
private GesturePath[] paths;
|
|
||||||
private MotionProcessor motionProcessor;
|
|
||||||
|
|
||||||
private double pathTime = 0.0D; // The timestamp at which the path is currently at.
|
|
||||||
private final AtomicInteger exerciseProgress = new AtomicInteger(0); // The progress of the exercise. Ranges from 0 to 1000.
|
|
||||||
|
|
||||||
private QiContext qiContext;
|
|
||||||
|
|
||||||
|
// Fields regarding Exercise and speech handling.
|
||||||
|
private InputProcessor motionProcessor;
|
||||||
private Exercise exercise;
|
private Exercise exercise;
|
||||||
|
private QiContext qiContext;
|
||||||
|
private int exerciseCount;
|
||||||
|
|
||||||
private Path targetPath; // The path the user is supposed to follow.
|
private FitnessActivity parentActivity;
|
||||||
private Path actualPath; // The path the user is currently following.
|
|
||||||
|
|
||||||
private final Paint referencePaint = new Paint();
|
private final Paint userProgressPaint = new Paint();
|
||||||
private final Paint targetPaint = new Paint();
|
private final Paint borderPaint = new Paint();
|
||||||
private final Paint backgroundColor = new Paint();
|
private final Paint backgroundPaint = new Paint();
|
||||||
|
|
||||||
|
// TODO: Remove
|
||||||
private Matrix4f viewMatrix = new Matrix4f(); // The view matrix for the 3D to 2D transformation.
|
private Matrix4f viewMatrix = new Matrix4f(); // The view matrix for the 3D to 2D transformation.
|
||||||
private Matrix4f projectionMatrix = new Matrix4f(); // The projection matrix for the 3D to 2D transformation.
|
private Matrix4f projectionMatrix = new Matrix4f(); // The projection matrix for the 3D to 2D transformation.
|
||||||
private final Vector4f objectPosition = new Vector4f(0, 0, 0, 1); // The location of the object in 3D space.
|
private final Vector4f objectPosition = new Vector4f(0, 0, 0, 1); // The location of the object in 3D space.
|
||||||
private ConcurrentLinkedQueue<Vector2f> vectors = new ConcurrentLinkedQueue<>();
|
private ConcurrentLinkedQueue<Vector2f> vectors = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
// TODO: Remove
|
||||||
private Vector2f[] axisVectors = new Vector2f[0];
|
private Vector2f[] axisVectors = new Vector2f[0];
|
||||||
|
|
||||||
|
|
||||||
|
private static final String[] STARTING_PHRASES = {
|
||||||
private static final String[] USER_PHRASES = {
|
|
||||||
"Veel success met de oefening!",
|
"Veel success met de oefening!",
|
||||||
"Je kan het!",
|
"Je kan het!",
|
||||||
"Veel plezier!"
|
"Veel plezier!"
|
||||||
};
|
};
|
||||||
|
|
||||||
private double timePassed = 0.0D; // The time that has passed since the start of the exercise, in seconds.
|
|
||||||
private long startingTime = 0L;
|
|
||||||
|
|
||||||
private Vector2f screenDimensions = new Vector2f(); // Width and height dimensions of the screen
|
|
||||||
|
|
||||||
public PersonalMotionPreviewElement(Context context, AttributeSet attrs) {
|
public PersonalMotionPreviewElement(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|
||||||
this.referencePaint.setColor(0xFFFF0000); // Red
|
if ( context instanceof Activity)
|
||||||
this.referencePaint.setStyle(Paint.Style.FILL);
|
{
|
||||||
this.referencePaint.setStrokeWidth(5.0f);
|
this.parentActivity = (FitnessActivity) context;
|
||||||
this.referencePaint.setAntiAlias(true);
|
}
|
||||||
|
|
||||||
|
this.userProgressPaint.setColor(0xFFFF0000); // Red
|
||||||
|
this.userProgressPaint.setStyle(Paint.Style.FILL);
|
||||||
|
this.userProgressPaint.setStrokeWidth(5.0f);
|
||||||
|
this.userProgressPaint.setAntiAlias(true);
|
||||||
|
|
||||||
// Target paint is the filling of the target path.
|
// Target paint is the filling of the target path.
|
||||||
this.targetPaint.setColor(-1);
|
this.borderPaint.setColor(-1);
|
||||||
this.targetPaint.setStyle(Paint.Style.STROKE);
|
this.borderPaint.setStyle(Paint.Style.STROKE);
|
||||||
this.targetPaint.setStrokeWidth(5.0f);
|
this.borderPaint.setStrokeWidth(5.0f);
|
||||||
this.targetPaint.setAntiAlias(true);
|
this.borderPaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
this.backgroundPaint.setColor(0xFF000000); // Black
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,34 +86,28 @@ public class PersonalMotionPreviewElement extends View {
|
|||||||
* will cause the vertex projections to fail (0 width and height).
|
* will cause the vertex projections to fail (0 width and height).
|
||||||
*
|
*
|
||||||
* @param exercise The exercise that the user is currently performing.
|
* @param exercise The exercise that the user is currently performing.
|
||||||
|
* @param motionProcessor The motion processor that will be used to process the user's motion.
|
||||||
|
* @param exerciseCount The total amount of exercises that the user has to perform.
|
||||||
*/
|
*/
|
||||||
public void initialize(Exercise exercise) {
|
public void initialize(@Nullable Exercise exercise, InputProcessor motionProcessor, int exerciseCount) {
|
||||||
Log.i("PersonalMotionPreviewElement", "Creating new PersonalMotionPreviewElement.");
|
Log.i("PersonalMotionPreviewElement", "Creating new PersonalMotionPreviewElement.");
|
||||||
this.backgroundColor.setColor(0xFF000000); // Black
|
|
||||||
|
|
||||||
this.screenDimensions.x = this.getWidth();
|
|
||||||
this.screenDimensions.y = this.getHeight();
|
|
||||||
this.actualPath = new Path();
|
|
||||||
this.targetPath = new Path();
|
|
||||||
|
|
||||||
this.startingTime = System.nanoTime(); // Set the last time to the current time
|
|
||||||
|
|
||||||
|
this.motionProcessor = motionProcessor;
|
||||||
this.exercise = exercise;
|
this.exercise = exercise;
|
||||||
this.paths = exercise.getPath();
|
this.exerciseCount = exerciseCount;
|
||||||
|
|
||||||
|
// TODO: Remove
|
||||||
this.axisVectors = new Vector2f[] {
|
this.axisVectors = new Vector2f[] {
|
||||||
|
projectVertex(new Vector3f(-5.0f, 0, 0), getWidth(), getHeight()),
|
||||||
projectVertex(new Vector3f(-100.0f, 0, 0), getWidth(), getHeight()),
|
projectVertex(new Vector3f(5.0f, 0, 0), getWidth(), getHeight()),
|
||||||
projectVertex(new Vector3f(100.0f, 0, 0), getWidth(), getHeight()),
|
projectVertex(new Vector3f(0, -5.0f, 0), getWidth(), getHeight()),
|
||||||
projectVertex(new Vector3f(0, -100.0f, 0), getWidth(), getHeight()),
|
projectVertex(new Vector3f(0, 5.0f, 0), getWidth(), getHeight()),
|
||||||
projectVertex(new Vector3f(0, 100.0f, 0), getWidth(), getHeight()),
|
projectVertex(new Vector3f(0, 0, -5.0f), getWidth(), getHeight()),
|
||||||
projectVertex(new Vector3f(0, 0, -100.0f), getWidth(), getHeight()),
|
projectVertex(new Vector3f(0, 0, 5.0f), getWidth(), getHeight())
|
||||||
projectVertex(new Vector3f(0, 0, 100.0f), getWidth(), getHeight())
|
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDestroy()
|
public void destroy()
|
||||||
{
|
{
|
||||||
if ( this.motionProcessor != null )
|
if ( this.motionProcessor != null )
|
||||||
this.motionProcessor.stopListening();
|
this.motionProcessor.stopListening();
|
||||||
@@ -128,35 +124,55 @@ public class PersonalMotionPreviewElement extends View {
|
|||||||
*/
|
*/
|
||||||
public void provideQiContext(QiContext context) {
|
public void provideQiContext(QiContext context) {
|
||||||
this.qiContext = context;
|
this.qiContext = context;
|
||||||
if ( this.motionProcessor != null )
|
|
||||||
this.motionProcessor.stopListening();
|
|
||||||
|
|
||||||
this.motionProcessor = new MotionProcessor();
|
|
||||||
this.motionProcessor.startListening();
|
|
||||||
|
|
||||||
// Handler that is called every time the motion processor receives new data.
|
// Handler that is called every time the motion processor receives new data.
|
||||||
this.motionProcessor.setMotionDataEventHandler((processed, preprocessed, sampleIndex, sampleRate, deviceId) -> {
|
this.motionProcessor.setInputHandler((rotationVector, deviceId) -> {
|
||||||
int progress = (int)this.motionProcessor.getError(this.paths[0], processed);
|
|
||||||
this.exerciseProgress.set(Math.min(1000, Math.max(0, progress)));
|
Log.i("MotionProcessor", "Rotation vector received: " + rotationVector);
|
||||||
Log.i("MotionProcessor", "Processed data: " + progress + " (" + preprocessed + ")");
|
Log.i("MotionProcessor", "Last error offset:" + this.motionProcessor.getError(deviceId, this.motionProcessor.secondsPassed()));
|
||||||
Vector2f parsed = projectVertex(processed, this.getWidth(), this.getHeight());
|
|
||||||
|
if ( this.motionProcessor.hasFinished())
|
||||||
|
{
|
||||||
|
if ( this.parentActivity == null)
|
||||||
|
{
|
||||||
|
// Move to main screen
|
||||||
|
this.destroy();
|
||||||
|
Intent intent = new Intent(getContext(), MainActivity.class);
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Move on to the next exercise, or finish.
|
||||||
|
if ( this.exerciseCount > 0 )
|
||||||
|
{
|
||||||
|
this.exerciseCount--;
|
||||||
|
this.exercise = this.parentActivity.acquireExercise();
|
||||||
|
this.motionProcessor.useExercise(this.exercise);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Finish the exercise.
|
||||||
|
this.destroy();
|
||||||
|
Intent intent = new Intent(getContext(), EndScreenActivity.class);
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Adjust / remove
|
||||||
|
vectors.add(projectVertex(rotationVector, this.getWidth(), this.getHeight()));
|
||||||
|
Log.i("MotionProcessor", "Rotation vector received: " + rotationVector);
|
||||||
|
Vector2f parsed = projectVertex(rotationVector, this.getWidth(), this.getHeight());
|
||||||
|
|
||||||
this.vectors.add(parsed);
|
this.vectors.add(parsed);
|
||||||
// Remove the first element if the array is too big
|
// Remove the first element if the array is too big
|
||||||
if (this.vectors.size() > 100)
|
if (this.vectors.size() > 100)
|
||||||
this.vectors.poll();
|
this.vectors.poll();
|
||||||
});
|
});
|
||||||
saySomethingNice();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to say something nice to the user :)
|
|
||||||
*/
|
|
||||||
private void saySomethingNice()
|
|
||||||
{
|
|
||||||
if (this.qiContext == null)
|
if (this.qiContext == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
FitnessCycle.say(USER_PHRASES[(int) Math.floor(Math.random() * USER_PHRASES.length)], this.qiContext);
|
FitnessCycle.say(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)], this.qiContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,6 +181,7 @@ public class PersonalMotionPreviewElement extends View {
|
|||||||
* @param exercise The exercise that the user is currently performing.
|
* @param exercise The exercise that the user is currently performing.
|
||||||
*/
|
*/
|
||||||
public void setExercise(Exercise exercise) {
|
public void setExercise(Exercise exercise) {
|
||||||
|
this.motionProcessor.useExercise(exercise);
|
||||||
this.exercise = exercise;
|
this.exercise = exercise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,12 +195,12 @@ public class PersonalMotionPreviewElement extends View {
|
|||||||
// Perspective transformation conserves the depth of the object
|
// Perspective transformation conserves the depth of the object
|
||||||
projectionMatrix
|
projectionMatrix
|
||||||
.identity()
|
.identity()
|
||||||
.perspective((float) Math.toRadians(70), (float) virtualWidth / virtualHeight, .01f, 10000.0f);
|
.perspective((float) Math.toRadians(70), (float) virtualWidth / virtualHeight, .01f, 1000.0f);
|
||||||
|
|
||||||
// Convert world coordinates to screen-space using MVP matrix
|
// Convert world coordinates to screen-space using MVP matrix
|
||||||
Vector4f screenCoordinates = new Vector4f(point, 1.0f)
|
Vector4f screenCoordinates = new Vector4f(point, 1.0f)
|
||||||
.mul(this.projectionMatrix)
|
.mul(this.viewMatrix)
|
||||||
.mul(this.viewMatrix);
|
.mul(this.projectionMatrix);
|
||||||
|
|
||||||
// Normalize screen coordinates from (-1, 1) to (0, virtualWidth) and (0, virtualHeight)
|
// Normalize screen coordinates from (-1, 1) to (0, virtualWidth) and (0, virtualHeight)
|
||||||
float normalizedX = (screenCoordinates.x / screenCoordinates.w + 1.0f) * 0.5f * virtualWidth;
|
float normalizedX = (screenCoordinates.x / screenCoordinates.w + 1.0f) * 0.5f * virtualWidth;
|
||||||
@@ -195,23 +212,23 @@ public class PersonalMotionPreviewElement extends View {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDraw(Canvas canvas) {
|
public void onDraw(Canvas canvas) {
|
||||||
canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundColor);
|
canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint);
|
||||||
this.setBackgroundColor(0xFF000000); // Black
|
this.setBackgroundColor(0xFF000000); // Black
|
||||||
/*if (this.exercise == null)
|
/*if (this.exercise == null)
|
||||||
return;*/
|
return;*/
|
||||||
|
|
||||||
for (int i = 0, startX, endX, startY, endY; i < axisVectors.length/2; i++)
|
for (int i = 0, startX, endX, startY, endY; i < axisVectors.length/2; i++)
|
||||||
{
|
{
|
||||||
startX = (int)Math.max(0, Math.min(this.screenDimensions.x, (int)axisVectors[i*2].x));
|
startX = (int)Math.max(0, Math.min(this.getWidth(), (int)axisVectors[i*2].x));
|
||||||
endX = (int)Math.max(0, Math.min(this.screenDimensions.x, (int)axisVectors[i*2+1].x));
|
endX = (int)Math.max(0, Math.min(this.getWidth(), (int)axisVectors[i*2+1].x));
|
||||||
startY = (int)Math.max(0, Math.min(this.screenDimensions.y, (int)axisVectors[i*2].y));
|
startY = (int)Math.max(0, Math.min(this.getHeight(), (int)axisVectors[i*2].y));
|
||||||
endY = (int)Math.max(0, Math.min(this.screenDimensions.y, (int)axisVectors[i*2+1].y));
|
endY = (int)Math.max(0, Math.min(this.getHeight(), (int)axisVectors[i*2+1].y));
|
||||||
canvas.drawLine(startX, startY, endX, endY, this.targetPaint);
|
canvas.drawLine(startX, startY, endX, endY, this.borderPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
for ( Vector2f point : this.vectors)
|
for ( Vector2f point : this.vectors)
|
||||||
{
|
{
|
||||||
canvas.drawRect(point.x, point.y, point.x + 5, point.y + 5, this.referencePaint);
|
canvas.drawRect(point.x, point.y, point.x + 5, point.y + 5, this.userProgressPaint);
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
// Draw target circle
|
// Draw target circle
|
||||||
@@ -228,7 +245,10 @@ public class PersonalMotionPreviewElement extends View {
|
|||||||
);*/
|
);*/
|
||||||
|
|
||||||
this.invalidate();
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(Vector3f rotationVector, int sensorId) {
|
||||||
|
|
||||||
timePassed = (System.nanoTime() - startingTime) / 1E9D;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -43,6 +43,19 @@ public class GesturePath {
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for retrieving the vectors of the GesturePath.
|
||||||
|
*/
|
||||||
|
public Vector3f[] getVectors()
|
||||||
|
{
|
||||||
|
Vector3f[] vectors = new Vector3f[segments.length + 1];
|
||||||
|
vectors[0] = segments[0].getStart();
|
||||||
|
for ( int i = 0; i < segments.length; i++)
|
||||||
|
vectors[i + 1] = segments[i].getEnd();
|
||||||
|
|
||||||
|
return vectors;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method for retrieving the closest path segment to a reference point.
|
* Method for retrieving the closest path segment to a reference point.
|
||||||
*
|
*
|
||||||
@@ -71,48 +84,40 @@ public class GesturePath {
|
|||||||
return closest(referencePoint).difference(referencePoint); // Get the closest segment and calculate the error.
|
return closest(referencePoint).difference(referencePoint); // Get the closest segment and calculate the error.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builder class for the GesturePath object.
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
// List of vectors to add to the GesturePath object.
|
|
||||||
private final List<Vector3f> vectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the Builder object.
|
* Function for converting a string to a GesturePath object.
|
||||||
|
* The input string bytes will be directly converted into 3d vectors.
|
||||||
|
* Every scalar is composed of 32 bits (4 characters), meaning 96 bits per vector.
|
||||||
*
|
*
|
||||||
* @param vectors The list of vectors to add.
|
* Note: ASCII to Vector conversion is done in Big Endian format (most significant byte first).
|
||||||
*/
|
|
||||||
public Builder(List<Vector3f> vectors) {
|
|
||||||
this.vectors = vectors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor for the Builder object.
|
|
||||||
*/
|
|
||||||
public Builder() {
|
|
||||||
this.vectors = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a vector to the GesturePath object.
|
|
||||||
*
|
*
|
||||||
* @param vector The vector to add.
|
* @param input The string to convert
|
||||||
* @return The Builder object.
|
* @return The GesturePath object
|
||||||
*/
|
*/
|
||||||
public Builder addVector(Vector3f vector) {
|
|
||||||
vectors.add(vector);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
public static GesturePath fromString(String input) {
|
||||||
* Builds the GesturePath object.
|
byte[] bytes = input.getBytes();
|
||||||
*
|
|
||||||
* @return The GesturePath object.
|
|
||||||
*/
|
|
||||||
public GesturePath build() {
|
|
||||||
return new GesturePath(vectors.toArray(new Vector3f[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Check if the input string contains a valid amount of bytes (12 bytes per vector)
|
||||||
|
if (input.length() % 12 != 0) {
|
||||||
|
throw new IllegalArgumentException("Invalid input string length");
|
||||||
}
|
}
|
||||||
|
Vector3f[] vectors = new Vector3f[input.length() / 12];
|
||||||
|
|
||||||
|
float[] xyz = new float[3];
|
||||||
|
for (int i = 0; i < bytes.length; i += 12) {
|
||||||
|
for (int j = 0; j < 3; j++) {
|
||||||
|
|
||||||
|
xyz[j] = Float.intBitsToFloat(
|
||||||
|
(bytes[i + j * 4] & 0xFF) << 24 |
|
||||||
|
(bytes[i + j * 4 + 1] & 0xFF) << 16 |
|
||||||
|
(bytes[i + j * 4 + 2] & 0xFF) << 8 |
|
||||||
|
(bytes[i + j * 4 + 3] & 0xFF)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
vectors[i / 12] = new Vector3f(xyz[0], xyz[1], xyz[2]);
|
||||||
|
}
|
||||||
|
return new GesturePath(vectors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
package com.example.fitbot.util.processing;
|
||||||
|
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
public interface IInputHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for accepting motion data and the transformed vector.
|
||||||
|
* @param rotationVector The rotation vector of the motion data.
|
||||||
|
* @param sensorId The sensor ID of the motion data.
|
||||||
|
*/
|
||||||
|
void accept(Vector3f rotationVector, int sensorId);
|
||||||
|
|
||||||
|
}
|
@@ -1,16 +0,0 @@
|
|||||||
package com.example.fitbot.util.processing;
|
|
||||||
|
|
||||||
import org.joml.Vector3f;
|
|
||||||
|
|
||||||
public interface IMotionDataConsumer {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for accepting motion data and the transformed vector.
|
|
||||||
* @param transformedVector The transformed vector.
|
|
||||||
* @param motionData The input motion data.
|
|
||||||
* @param sampleIndex The index of the current sample
|
|
||||||
* @param sampleRate The sample rate.
|
|
||||||
*/
|
|
||||||
void accept(Vector3f transformedVector, MotionData motionData, int sampleIndex, double sampleRate, int sensorId);
|
|
||||||
|
|
||||||
}
|
|
@@ -0,0 +1,236 @@
|
|||||||
|
package com.example.fitbot.util.processing;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.example.fitbot.exercise.Exercise;
|
||||||
|
import com.example.fitbot.util.server.WebServer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
public class InputProcessor {
|
||||||
|
|
||||||
|
private Vector3f[][] selfRotationVectorPaths; // Relative path of the motion data
|
||||||
|
private Vector3f[][] targetRotationVectorPaths; // Target path of the motion data
|
||||||
|
|
||||||
|
private final float sampleRate; // The sample rate of the motion sensor
|
||||||
|
private float exerciseDuration;
|
||||||
|
|
||||||
|
// How many seconds have passed since the start of the exercise.
|
||||||
|
// The exercise starts whenever the `startListening` function is called.
|
||||||
|
private double secondsPassed = 0.0D;
|
||||||
|
private long lastTime;
|
||||||
|
|
||||||
|
private IInputHandler motionDataConsumer = (rotationVector, deviceId) -> {
|
||||||
|
};
|
||||||
|
private WebServer server;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the motion processor.
|
||||||
|
*
|
||||||
|
* @param paths The target paths of the motion data.
|
||||||
|
* The length of this array must be equal to the
|
||||||
|
* amount of sensors available.
|
||||||
|
* @param inputSampleRate The sample rate of the motion sensor.
|
||||||
|
*/
|
||||||
|
public InputProcessor(Vector3f[][] paths, float exerciseTime, float inputSampleRate) {
|
||||||
|
selfRotationVectorPaths = new Vector3f[paths.length][(int) (exerciseTime * inputSampleRate)];
|
||||||
|
targetRotationVectorPaths = paths;
|
||||||
|
|
||||||
|
this.sampleRate = inputSampleRate;
|
||||||
|
this.exerciseDuration = exerciseTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for setting the exercise to use.
|
||||||
|
* This updates the user and target path and the
|
||||||
|
* duration of the exercise.
|
||||||
|
* @param exercise The exercise to use the paths for.
|
||||||
|
*/
|
||||||
|
public void useExercise(Exercise exercise) {
|
||||||
|
this.selfRotationVectorPaths = new Vector3f[2][(int) (exercise.exerciseTimeInSeconds * this.sampleRate)];
|
||||||
|
this.targetRotationVectorPaths = new Vector3f[2][exercise.rightPath.getVectors().length];
|
||||||
|
this.exerciseDuration = exercise.exerciseTimeInSeconds;
|
||||||
|
this.secondsPassed = 0.0D;
|
||||||
|
this.lastTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for checking if the exercise has finished.
|
||||||
|
* @return True if the exercise has finished, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean hasFinished() {
|
||||||
|
return this.secondsPassed >= this.exerciseDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for starting the listening process
|
||||||
|
* of the motion sensor. This function will create
|
||||||
|
* a new WebSocket server and start listening for
|
||||||
|
* incoming connections.
|
||||||
|
*/
|
||||||
|
public void startListening() {
|
||||||
|
// Create socket server
|
||||||
|
this.server = WebServer.createServer();
|
||||||
|
|
||||||
|
Log.i("MotionProcessor", "Listening for incoming connections.");
|
||||||
|
|
||||||
|
// Check if the socket
|
||||||
|
if (server != null) {
|
||||||
|
// Update event handler to match our functionality.
|
||||||
|
server.setEventHandler(this::parsePacket);
|
||||||
|
this.secondsPassed = 0.0d;
|
||||||
|
this.lastTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for stopping the listening process
|
||||||
|
* of the motion sensor. This function will stop
|
||||||
|
* the WebSocket server.
|
||||||
|
*/
|
||||||
|
public void stopListening() {
|
||||||
|
if (server != null) {
|
||||||
|
server.stop();
|
||||||
|
server = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for parsing arbitrary packet data.
|
||||||
|
*
|
||||||
|
* @param data The data to parse.
|
||||||
|
*/
|
||||||
|
public void parsePacket(@NotNull String data) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
Log.i("MotionProcessor", "Received packet data: " + data);
|
||||||
|
|
||||||
|
JsonElement json = JsonParser.parseString(data);
|
||||||
|
|
||||||
|
if (!json.isJsonObject())
|
||||||
|
return;
|
||||||
|
|
||||||
|
JsonObject object = json.getAsJsonObject();
|
||||||
|
|
||||||
|
String[] required = {
|
||||||
|
"rotationX", "rotationY", "rotationZ",
|
||||||
|
"type",
|
||||||
|
"deviceId"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure all properties are present in the received JSON object
|
||||||
|
for (String s : required) {
|
||||||
|
if (!object.has(s))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the data
|
||||||
|
Vector3f rotation = new Vector3f(object.get("rotationX").getAsFloat(), object.get("rotationY").getAsFloat(), object.get("rotationZ").getAsFloat());
|
||||||
|
int deviceId = object.get("deviceId").getAsInt();
|
||||||
|
String type = object.get("type").getAsString();
|
||||||
|
|
||||||
|
parseRotationVector(rotation, deviceId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.i("MotionProcessor", "Failed to parse packet data.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for adding motion data to the processor.
|
||||||
|
*
|
||||||
|
* @param rotation The rotation vector of the motion data.
|
||||||
|
* @param deviceId The device ID of the motion data.
|
||||||
|
*/
|
||||||
|
public void parseRotationVector(Vector3f rotation, int deviceId) {
|
||||||
|
if (deviceId >= 0 && deviceId < selfRotationVectorPaths.length) {
|
||||||
|
|
||||||
|
// Re-calculate time for index calculation
|
||||||
|
secondsPassed = (System.currentTimeMillis() - lastTime) / 1000.0d;
|
||||||
|
lastTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Supposed index of the current rotation vector in the `rotationVectorPaths` array
|
||||||
|
int selfIndex = (int) (secondsPassed * sampleRate);
|
||||||
|
|
||||||
|
motionDataConsumer.accept(rotation, deviceId);
|
||||||
|
if (selfIndex >= selfRotationVectorPaths[deviceId].length || selfIndex < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selfRotationVectorPaths[deviceId][selfIndex] = rotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for getting the current progress of the exercise.
|
||||||
|
* The return value will range between 0.0 and 1.0.
|
||||||
|
*
|
||||||
|
* @return The current progress of the exercise.
|
||||||
|
*/
|
||||||
|
public double getCurrentProgress()
|
||||||
|
{
|
||||||
|
return secondsPassed / exerciseDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for setting the motion data receiver.
|
||||||
|
*
|
||||||
|
* @param consumer The consumer to set.
|
||||||
|
*/
|
||||||
|
public void setInputHandler(IInputHandler consumer) {
|
||||||
|
if (consumer != null)
|
||||||
|
this.motionDataConsumer = consumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for getting the error offsets of the user's path compared to the
|
||||||
|
* target path at a given point in time.
|
||||||
|
*
|
||||||
|
* @param sensorId The sensor ID to get the error offsets from.
|
||||||
|
* @param time The time to get the error offsets from.
|
||||||
|
* This value must be >= 0 && <= exerciseTime, otherwise
|
||||||
|
* the error will be 0 by default.
|
||||||
|
* @return A list of error offsets of the motion data compared to the reference path.
|
||||||
|
*/
|
||||||
|
public double getError(int sensorId, float time) {
|
||||||
|
|
||||||
|
// Ensure the sensor ID is within the bounds of the array
|
||||||
|
if (sensorId < 0 || sensorId >= selfRotationVectorPaths.length)
|
||||||
|
return 0.0d;
|
||||||
|
|
||||||
|
// Index of the current rotation vector
|
||||||
|
int targetIndex = (int) ((this.exerciseDuration / this.targetRotationVectorPaths[sensorId].length) * time);
|
||||||
|
int selfIndex = (int) (this.selfRotationVectorPaths[sensorId].length / this.sampleRate * time);
|
||||||
|
|
||||||
|
// Ensure the indexes are within the bounds of the array
|
||||||
|
if (targetIndex >= 0 && targetIndex <= this.targetRotationVectorPaths[sensorId].length - 1 &&
|
||||||
|
selfIndex >= 0 && selfIndex <= this.selfRotationVectorPaths[sensorId].length - 1)
|
||||||
|
{
|
||||||
|
return this.selfRotationVectorPaths[sensorId][selfIndex].distance(this.targetRotationVectorPaths[sensorId][targetIndex]);
|
||||||
|
}
|
||||||
|
return 0.0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for getting the average error of the motion data
|
||||||
|
* compared to the reference path.
|
||||||
|
*
|
||||||
|
* @param sensorId The sensor ID to get the error offsets from.
|
||||||
|
* @return The average error of the motion data compared to the reference path.
|
||||||
|
*/
|
||||||
|
public double getAverageError(int sensorId) {
|
||||||
|
double error = 0;
|
||||||
|
for (int i = 0; i < this.exerciseDuration; i++) {
|
||||||
|
error += getError(sensorId, i);
|
||||||
|
}
|
||||||
|
return error / this.exerciseDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float secondsPassed() {
|
||||||
|
return (float) secondsPassed;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,68 +0,0 @@
|
|||||||
package com.example.fitbot.util.processing;
|
|
||||||
|
|
||||||
import org.joml.Vector3f;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class MotionData {
|
|
||||||
|
|
||||||
// Data of the motion sensor
|
|
||||||
public Vector3f acceleration, rotation;
|
|
||||||
public int sensorId;
|
|
||||||
|
|
||||||
// Delimiter for the data received from the motion sensor
|
|
||||||
private static final String DATA_DELIMITER = ";";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for the MotionData class.
|
|
||||||
*
|
|
||||||
* @param accelerationX The acceleration in the X axis in m/s^2.
|
|
||||||
* @param accelerationY The acceleration in the Y axis in m/s^2.
|
|
||||||
* @param accelerationZ The acceleration in the Z axis in m/s^2.
|
|
||||||
* @param rotationX The rotation in the X axis in degrees.
|
|
||||||
* @param rotationY The rotation in the Y axis in degrees.
|
|
||||||
* @param rotationZ The rotation in the Z axis in degrees.
|
|
||||||
* @param sensorId The sensor id.
|
|
||||||
*/
|
|
||||||
public MotionData(float accelerationX, float accelerationY, float accelerationZ, float rotationX, float rotationY, float rotationZ, int sensorId) {
|
|
||||||
this(new Vector3f(accelerationX, accelerationY, accelerationZ), new Vector3f(rotationX, rotationY, rotationZ), sensorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for the MotionData class.
|
|
||||||
*
|
|
||||||
* @param acceleration The acceleration vector in m/s^2.
|
|
||||||
* @param rotation The rotation vector in degrees.
|
|
||||||
*/
|
|
||||||
public MotionData(Vector3f acceleration, Vector3f rotation, int sensorId) {
|
|
||||||
this.acceleration = acceleration;
|
|
||||||
this.rotation = rotation;
|
|
||||||
this.sensorId = sensorId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for decoding a string into a MotionData object.
|
|
||||||
* This string must contain the data of the motion sensor
|
|
||||||
* separated by the delimiter. (;)
|
|
||||||
*
|
|
||||||
* @param data The string containing the data of the motion sensor.
|
|
||||||
* @return An instance of MotionData.
|
|
||||||
*/
|
|
||||||
public static MotionData decode(String data) {
|
|
||||||
Objects.requireNonNull(data); // Ensure data is not null
|
|
||||||
|
|
||||||
String[] parts = data.split(DATA_DELIMITER);
|
|
||||||
if (parts.length != 7)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new MotionData(
|
|
||||||
Float.parseFloat(parts[0]),
|
|
||||||
Float.parseFloat(parts[1]),
|
|
||||||
Float.parseFloat(parts[2]),
|
|
||||||
Float.parseFloat(parts[3]),
|
|
||||||
Float.parseFloat(parts[4]),
|
|
||||||
Float.parseFloat(parts[5]),
|
|
||||||
Integer.parseInt(parts[6])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,233 +0,0 @@
|
|||||||
package com.example.fitbot.util.processing;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.example.fitbot.util.path.GesturePath;
|
|
||||||
import com.example.fitbot.util.server.WebServer;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.joml.Matrix3d;
|
|
||||||
import org.joml.Vector3d;
|
|
||||||
import org.joml.Vector3f;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class MotionProcessor {
|
|
||||||
|
|
||||||
public static final String DELIMITER = ";";
|
|
||||||
|
|
||||||
private final List<Vector3f> relativeLeftPath = new ArrayList<>(); // Relative path of the left motion data
|
|
||||||
private final List<Vector3f> relativeRightPath = new ArrayList<>(); // Relative path of the motion data
|
|
||||||
|
|
||||||
private Vector3f ZERO = new Vector3f(0, 0, 0);
|
|
||||||
|
|
||||||
private final float sampleRate = 1.0f / 10.0F; // samples/second
|
|
||||||
|
|
||||||
private IMotionDataConsumer motionDataConsumer = (p1, p2, p3, p4, p5) -> { };
|
|
||||||
private WebServer server;
|
|
||||||
|
|
||||||
|
|
||||||
public MotionProcessor() {}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for starting the listening process
|
|
||||||
* of the motion sensor. This function will create
|
|
||||||
* a new WebSocket server and start listening for
|
|
||||||
* incoming connections.
|
|
||||||
*/
|
|
||||||
public void startListening() {
|
|
||||||
// Create socket server
|
|
||||||
this.server = WebServer.createServer();
|
|
||||||
|
|
||||||
Log.i("MotionProcessor", "Listening for incoming connections.");
|
|
||||||
|
|
||||||
// Check if the socket
|
|
||||||
if (server != null) {
|
|
||||||
// Update event handler to match our functionality.
|
|
||||||
server.setEventHandler(this::parsePacket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for stopping the listening process
|
|
||||||
* of the motion sensor. This function will stop
|
|
||||||
* the WebSocket server.
|
|
||||||
*/
|
|
||||||
public void stopListening() {
|
|
||||||
if (server != null) {
|
|
||||||
server.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for parsing arbitrary packet data.
|
|
||||||
*
|
|
||||||
* @param data The data to parse.
|
|
||||||
*/
|
|
||||||
public void parsePacket(@NotNull String data) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
Log.i("MotionProcessor", "Received packet data: " + data);
|
|
||||||
|
|
||||||
JsonElement json = JsonParser.parseString(data);
|
|
||||||
|
|
||||||
if (!json.isJsonObject())
|
|
||||||
return;
|
|
||||||
|
|
||||||
JsonObject object = json.getAsJsonObject();
|
|
||||||
|
|
||||||
String[] required = {
|
|
||||||
"rotationX", "rotationY", "rotationZ",
|
|
||||||
"accelerationX", "accelerationY", "accelerationZ",
|
|
||||||
"type",
|
|
||||||
"deviceId"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure all properties are present in the received JSON object
|
|
||||||
for (String s : required) {
|
|
||||||
if (!object.has(s))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the data
|
|
||||||
Vector3f rotation = new Vector3f(object.get("rotationX").getAsFloat(), object.get("rotationY").getAsFloat(), object.get("rotationZ").getAsFloat());
|
|
||||||
Vector3f acceleration = new Vector3f(object.get("accelerationX").getAsFloat(), object.get("accelerationY").getAsFloat(), object.get("accelerationZ").getAsFloat());
|
|
||||||
int deviceId = object.get("deviceId").getAsInt();
|
|
||||||
String type = object.get("type").getAsString();
|
|
||||||
MotionData motionData = new MotionData(rotation, acceleration, deviceId);
|
|
||||||
|
|
||||||
if (type.equals("calibrate")) {
|
|
||||||
ZERO = getRelativeVector(motionData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addMotionData(motionData);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.i("MotionProcessor", "Failed to parse packet data.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for adding motion data to the processor.
|
|
||||||
*
|
|
||||||
* @param data The motion data to add.
|
|
||||||
*/
|
|
||||||
public void addMotionData(MotionData data) {
|
|
||||||
List<Vector3f> target;
|
|
||||||
if (data.sensorId == 0)
|
|
||||||
target = relativeLeftPath;
|
|
||||||
else target = relativeRightPath;
|
|
||||||
Vector3f previous = target.isEmpty() ? ZERO : target.get(target.size() - 1);
|
|
||||||
Vector3f relativeVector = getRelativeVector(data).add(previous);
|
|
||||||
target.add(relativeVector);
|
|
||||||
motionDataConsumer.accept(relativeVector, data, target.size(), this.sampleRate, data.sensorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for updating the relative path.
|
|
||||||
*
|
|
||||||
* @param relativeRightPath The new relative path.
|
|
||||||
*/
|
|
||||||
public void setRelativePaths(List<Vector3f> relativeLeftPath, List<Vector3f> relativeRightPath) {
|
|
||||||
this.relativeRightPath.clear();
|
|
||||||
this.relativeLeftPath.clear();
|
|
||||||
this.relativeLeftPath.addAll(relativeLeftPath);
|
|
||||||
this.relativeRightPath.addAll(relativeRightPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for setting the motion data receiver.
|
|
||||||
*
|
|
||||||
* @param consumer The consumer to set.
|
|
||||||
*/
|
|
||||||
public void setMotionDataEventHandler(IMotionDataConsumer consumer) {
|
|
||||||
if (consumer != null)
|
|
||||||
this.motionDataConsumer = consumer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for getting the relative vector of the motion data.
|
|
||||||
* This function will calculate the relative position of the motion data
|
|
||||||
* based on its acceleration and rotation vectors. This has to be done since
|
|
||||||
* the acceleration vector is relative to its own rotation vector.
|
|
||||||
*
|
|
||||||
* @param motionData The motion data to calculate the relative vector for.
|
|
||||||
* @return The relative vector of the motion data.
|
|
||||||
*/
|
|
||||||
public Vector3f getRelativeVector(MotionData motionData) {
|
|
||||||
|
|
||||||
// Rotate the acceleration vector back by the rotation vector to make it
|
|
||||||
// perpendicular to the gravity vector, then apply double integration to get the relative position.
|
|
||||||
// s = 1/2 * a * t^2
|
|
||||||
// Step 2: Create rotation matrices for each axis
|
|
||||||
// Step 4: Rotate the acceleration vector
|
|
||||||
|
|
||||||
return motionData.rotation
|
|
||||||
.mul(5);
|
|
||||||
/*return motionData.acceleration
|
|
||||||
.rotateZ(-motionData.rotation.z)
|
|
||||||
.rotateY(-motionData.rotation.y)
|
|
||||||
.rotateX(-motionData.rotation.x)
|
|
||||||
.mul(sampleRate * sampleRate / 2);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for getting the error offsets of the provided path and the
|
|
||||||
* received motion data.
|
|
||||||
*
|
|
||||||
* @param referencePath The reference path to compare the motion data to.
|
|
||||||
* @return A list of error offsets of the motion data compared to the reference path.
|
|
||||||
*/
|
|
||||||
public List<Double> getErrors(GesturePath referencePath) {
|
|
||||||
|
|
||||||
List<Double> errors = new ArrayList<>();
|
|
||||||
for (Vector3f vector : relativeRightPath) {
|
|
||||||
errors.add(referencePath.getError(vector));
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for getting the error of the motion data compared to the reference path.
|
|
||||||
*
|
|
||||||
* @param path The path to compare the motion data to.
|
|
||||||
* @param referencePoint The reference point to compare the motion data to.
|
|
||||||
* @return The error of the motion data compared to the reference path.
|
|
||||||
*/
|
|
||||||
public double getError(GesturePath path, Vector3f referencePoint) {
|
|
||||||
return path.getError(referencePoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for calculating the average error of the motion data
|
|
||||||
* compared to the reference path.
|
|
||||||
*
|
|
||||||
* @param referencePath The reference path to compare the motion data to.
|
|
||||||
* @return The average error of the motion data compared to the reference path.
|
|
||||||
*/
|
|
||||||
public double getAverageError(GesturePath referencePath, int sensorId) {
|
|
||||||
double error = 0;
|
|
||||||
for (Double e : getErrors(referencePath)) {
|
|
||||||
error += e;
|
|
||||||
}
|
|
||||||
return error / Math.max(1, (sensorId == 0 ? relativeLeftPath : relativeRightPath).size());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for logging the statistics of the motion data.
|
|
||||||
*
|
|
||||||
* @param referencePath The reference path to compare the motion data to.
|
|
||||||
*/
|
|
||||||
public void logStatistics(GesturePath referencePath) {
|
|
||||||
Log.i("MotionProcessor", "Path length: " + relativeRightPath.size());
|
|
||||||
Log.i("MotionProcessor", "Sample rate: " + sampleRate);
|
|
||||||
Log.i("MotionProcessor", "Calibration point: " + ZERO.toString());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -22,7 +22,7 @@ public class WebServer implements Runnable {
|
|||||||
protected IWebServerHandler eventHandler = (input) -> {}; // No-op.
|
protected IWebServerHandler eventHandler = (input) -> {}; // No-op.
|
||||||
|
|
||||||
private Thread thread;
|
private Thread thread;
|
||||||
private AtomicBoolean forceClose = new AtomicBoolean(false);
|
private final AtomicBoolean forceClose = new AtomicBoolean(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for creating a new WebSocket server.
|
* Constructor for creating a new WebSocket server.
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
package com.example.fitbot;
|
||||||
|
|
||||||
|
import com.example.fitbot.exercise.EMuscleGroup;
|
||||||
|
import com.example.fitbot.exercise.Exercise;
|
||||||
|
import com.example.fitbot.exercise.ExerciseManager;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class DatabaseFetchingTest {
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDatabaseFetching() {
|
||||||
|
Exercise exercise = ExerciseManager.retrieveExercise();
|
||||||
|
assert exercise != null;
|
||||||
|
System.out.println("\n---------------------------------");
|
||||||
|
System.out.println("Exercise:");
|
||||||
|
System.out.println("Name: " + exercise.title);
|
||||||
|
System.out.println("Description: " + exercise.description);
|
||||||
|
System.out.println("Muscle Group: " + exercise.muscleGroup);
|
||||||
|
System.out.println("Image URL: " + exercise.imageUrl);
|
||||||
|
System.out.println("Video URL: " + exercise.videoUrl);
|
||||||
|
System.out.println("Exercise Time: " + exercise.exerciseTimeInSeconds);
|
||||||
|
System.out.println("\n---------------------------------");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user