Removed ExerciseStatusElement.java, moved all functionality to InputProcessor.java

This commit is contained in:
Luca Warmenhoven
2024-06-05 12:49:39 +02:00
parent d46db1a525
commit 7782722310
5 changed files with 116 additions and 236 deletions

View File

@@ -40,6 +40,12 @@ public class ExerciseManager {
public static final float EXERCISE_ERROR_MARGIN = 1.0f;
public static final float EXERCISE_TIME_SCALING_FACTOR = 1.0f;
// Fields representing the statistics of the user
public static int TOTAL_REPETITIONS_REQUIRED = 0;
public static int TOTAL_REPETITIONS_PERFORMED = 0;
public static int TOTAL_EXERCISES_PREFORMED = 0;
/**
* Function for sending an HTTP request to the server.
*
@@ -60,6 +66,7 @@ public class ExerciseManager {
// Send a body if it is present
if (body != null)
connection.getOutputStream().write(body.getBytes());
InputStream stream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
StringBuilder builder = new StringBuilder();

View File

@@ -6,14 +6,12 @@ import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.VideoView;
import com.aldebaran.qi.sdk.QiContext;
@@ -25,16 +23,12 @@ import com.example.fitbot.R;
import com.example.fitbot.exercise.Exercise;
import com.example.fitbot.exercise.ExerciseManager;
import com.example.fitbot.pepper.Pepper;
import com.example.fitbot.ui.components.ExerciseStatusElement;
import com.example.fitbot.util.NavigationManager;
import com.example.fitbot.util.processing.InputProcessor;
import org.joml.Vector3f;
public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks {
// Private fields for the FitnessActivity class.
private ExerciseStatusElement exerciseStatusElement;
private InputProcessor motionProcessor;
private Exercise currentExercise;
@@ -104,30 +98,22 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
// Provide the context so that all queued actions can be performed.
Pepper.provideContext(qiContext, this.getClass());
exerciseStatusElement = 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.
exerciseStatusElement.post(() -> {
this.fetchExerciseAsync((exercise) -> {
// Acquire paths from the exercise and provide them to the motion processor
Vector3f[][] vectors = new Vector3f[][]{exercise.leftPath.getAngleVectors(), exercise.rightPath.getAngleVectors()};
this.fetchExerciseAsync((exercise) -> {
// Acquire paths from the exercise and provide them to the motion processor
motionProcessor = new InputProcessor(SENSOR_SAMPLE_RATE, this);
motionProcessor.useExercise(exercise);
/* TODO: Remove if not needed */
motionProcessor.setRecording(true, 10);
motionProcessor.startListening();
motionProcessor = new InputProcessor(vectors, exercise.exerciseTimeInSeconds, SENSOR_SAMPLE_RATE);
exerciseStatusElement.initialize(exercise, motionProcessor, EXERCISE_COUNT);
motionProcessor.useExercise(exercise);
/* TODO: Remove if not needed */motionProcessor.setRecording(true, 10);
motionProcessor.setInputHandler(exerciseStatusElement);
motionProcessor.startListening();
}, (n) -> {
int randomMessageIndex = (int) Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length);
Pepper.say(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex]);
Pepper.say(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE);
NavigationManager.navigateToActivity(this, EndScreenActivity.class);
});
}, (n) -> {
int randomMessageIndex = (int) Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length);
Pepper.say(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex]);
Pepper.say(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE);
NavigationManager.navigateToActivity(this, EndScreenActivity.class);
});
}
@@ -165,13 +151,10 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
return false;
});
videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
if (EXERCISE_REP < EXERCISE_COUNT) {
videoView.start(); // start the video again
EXERCISE_REP++;
}
videoView.setOnCompletionListener(mp -> {
if (EXERCISE_REP < EXERCISE_COUNT) {
videoView.start(); // start the video again
EXERCISE_REP++;
}
});
});

View File

@@ -1,159 +0,0 @@
package com.example.fitbot.ui.components;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.example.fitbot.exercise.Exercise;
import com.example.fitbot.pepper.Pepper;
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.NavigationManager;
import com.example.fitbot.util.processing.IInputHandler;
import com.example.fitbot.util.processing.InputProcessor;
import org.joml.Vector3f;
public class ExerciseStatusElement extends View implements IInputHandler {
// Fields regarding Exercise and speech handling.
private InputProcessor motionProcessor;
private Exercise exercise;
private int exerciseCount;
private FitnessActivity parentActivity;
private final Paint userProgressPaint = new Paint();
private final Paint borderPaint = new Paint();
private final Paint backgroundPaint = new Paint();
private static final String[] STARTING_PHRASES = {
"Veel success met de oefening!",
"Je kan het!",
"Veel plezier!"
};
public ExerciseStatusElement(Context context, AttributeSet attrs) {
super(context, attrs);
if (context instanceof Activity) {
this.parentActivity = (FitnessActivity) context;
}
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.
this.borderPaint.setColor(-1);
this.borderPaint.setStyle(Paint.Style.STROKE);
this.borderPaint.setStrokeWidth(5.0f);
this.borderPaint.setAntiAlias(true);
this.backgroundPaint.setColor(0xFF000000); // Black
}
/**
* Method for initializing the PersonalMotionPreviewElement.
* This method has to be called with a "post" function when the element has been
* created, otherwise the dimensions of the element aren't initialized yet, which
* will cause the vertex projections to fail (0 width and height).
*
* @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(@Nullable Exercise exercise, InputProcessor motionProcessor, int exerciseCount) {
Log.i("PersonalMotionPreviewElement", "Creating new PersonalMotionPreviewElement.");
this.motionProcessor = motionProcessor;
this.exercise = exercise;
this.exerciseCount = exerciseCount;
Pepper.say(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)]);
// Handler that is called every time the motion processor receives new data.
}
/**
* Method for setting the gesture path that will be drawn on the canvas.
*
* @param exercise The exercise that the user is currently performing.
*/
public void setExercise(Exercise exercise) {
this.motionProcessor.useExercise(exercise);
this.exercise = exercise;
Log.i("MotionProcessor", "Updating exercise in ExerciseStatusElement");
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint);
this.setBackgroundColor(0xFF000000); // Black
/*if (this.exercise == null)
return;*/
/*
// Draw target circle
float targetRadius = (this.screenDimensions.x + this.screenDimensions.y) / 5.0f;
canvas.drawCircle(this.screenDimensions.x / 2, this.screenDimensions.y / 2, targetRadius, this.targetPaint);
canvas.drawCircle(this.screenDimensions.x / 2, this.screenDimensions.y / 2, (targetRadius * exerciseProgress.get()/1000.0f), this.referencePaint);
referencePaint.setColor(
Color.argb(
255,
(int)(255 * (1.0 - exerciseProgress.get()/1000.0f)),
(int)(255 * exerciseProgress.get()/1000.0f),
0
)
);*/
this.invalidate();
}
@Override
public void accept(Vector3f rotationVector, int sensorId) {
Log.i("MotionProcessor", "Rotation vector received: " + rotationVector);
Log.i("MotionProcessor", "Last error offset:" + this.motionProcessor.getError(sensorId, this.motionProcessor.secondsPassed()));
// Check whether the current exercise has been completed.
// This is determined by the duration of the exercise, and the amount of time that has passed.
// The duration of the exercise originates from the database, and is stored in seconds.
// Whenever 'useExercise' is called, the timer resets and this method will be called again.
if (this.motionProcessor.hasFinished() && !this.motionProcessor.isRecording()) {
// If for some reason the parent activity is not defined,
// move back to the main screen.
if (this.parentActivity == null) {
// Move to main screen
Log.i("MotionProcessor", "Parent activity was null.");
NavigationManager.navigateToActivity(getContext(), MainActivity.class);
return;
}
// Move on to the next exercise, or finish.
if (this.exerciseCount > 0) {
this.exerciseCount--;
this.parentActivity.fetchExerciseAsync((newExercise) -> {
this.motionProcessor.useExercise(newExercise);
// Whenever the database retrieval failed, we return to the main screen.
}, (failed) -> {
// Move to main screen
Log.i("MotionProcessor", "Failed to fetch exercise from database");
NavigationManager.navigateToActivity(parentActivity, MainActivity.class);
});
} else {
// Finish the exercise.
Log.i("MotionProcessor", "Exercise has finished");
NavigationManager.navigateToActivity(parentActivity, EndScreenActivity.class);
}
}
}
}

View File

@@ -53,13 +53,15 @@ public class AnglePath {
throw new IllegalArgumentException("Input string must contain 2 elements");
Vector3f[][] angles = new Vector3f[ExerciseManager.SENSOR_COUNT][];
for ( int dataArrayIdx = 0; dataArrayIdx < parsed.getAsJsonArray().size(); dataArrayIdx++)
{
JsonArray array = parsed.getAsJsonArray().get(dataArrayIdx).getAsJsonObject().get("data").getAsJsonArray();
angles[dataArrayIdx] = new Vector3f[array.size()];
int deviceIdx = parsed.getAsJsonArray().get(dataArrayIdx).getAsJsonObject().get("deviceId").getAsInt();
angles[deviceIdx] = new Vector3f[array.size()];
for (int i = 0; i < array.size(); i++) {
JsonArray vec = array.get(i).getAsJsonArray();
angles[dataArrayIdx][i] = new Vector3f(vec.get(0).getAsFloat(), vec.get(1).getAsFloat(), vec.get(2).getAsFloat());
angles[deviceIdx][i] = new Vector3f(vec.get(0).getAsFloat(), vec.get(1).getAsFloat(), vec.get(2).getAsFloat());
}
}
return new AnglePath[] {new AnglePath(angles[0]), new AnglePath(angles[1])};

View File

@@ -2,15 +2,18 @@ package com.example.fitbot.util.processing;
import android.util.Log;
import com.aldebaran.qi.sdk.object.geometry.Vector3;
import com.example.fitbot.exercise.Exercise;
import com.example.fitbot.exercise.ExerciseManager;
import com.example.fitbot.pepper.Pepper;
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.NavigationManager;
import com.example.fitbot.util.server.WebServer;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import org.jetbrains.annotations.NotNull;
import org.joml.Vector3f;
@@ -26,9 +29,21 @@ public class InputProcessor {
private final float sampleRate; // The sample rate of the motion sensor
private float exerciseDurationInSeconds;
private int repetitionsRemaining = 0;
private int exercisesRemaining = 0;
private float exerciseScore = 0.0F;
private final FitnessActivity parentActivity;
/**
* The phrases that are said by the robot whenever the exercise starts.
*/
private static final String[] STARTING_PHRASES = {
"Veel success met de oefening!",
"Je kan het!",
"Veel plezier!"
};
/**
* This field is used to determine if the motion data is being recorded.
* If this is the case, instead of functioning normally, the element
@@ -48,8 +63,6 @@ public class InputProcessor {
private double secondsPassed = 0.0D;
private long lastTime;
private IInputHandler motionDataConsumer;
private static final String[] REQUIRED_SENSOR_JSON_PROPERTIES =
{"rotationX", "rotationY", "rotationZ", "deviceId"};
@@ -60,34 +73,50 @@ public class InputProcessor {
/**
* 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) {
this.selfRotationVectorPaths = new ArrayList[2];
this.selfRotationVectorPaths[0] = new ArrayList<>();
this.selfRotationVectorPaths[1] = new ArrayList<>();
targetRotationVectorPaths = paths;
public InputProcessor(float inputSampleRate, FitnessActivity parentActivity) {
this.sampleRate = inputSampleRate;
this.exerciseDurationInSeconds = exerciseTime;
this.parentActivity = parentActivity;
}
/**
* Function for setting the exercise to use.
* This updates the user and target path and the
* duration of the exercise.
* <p>
* This function is only initially used to select the starting exercise;
* the exercises that follow are determined by a private method 'nextExercise'
*
* @param exercise The exercise to use the paths for.
*/
public void useExercise(Exercise exercise) {
if ( this.recordingMovement )
if (this.recordingMovement)
throw new IllegalStateException("Cannot change exercise while recording movement.");
this.exercisesRemaining = 1;
this.nextExercise(exercise);
Pepper.say(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)]);
}
/**
* Moves on to the next exercise without changing the remaining exercises.
*
* @param exercise The exercise to move on to.
*/
private void nextExercise(Exercise exercise) {
if (this.exercisesRemaining-- <= 0) {
NavigationManager.navigateToActivity(this.parentActivity, EndScreenActivity.class);
}
ExerciseManager.TOTAL_REPETITIONS_REQUIRED += ExerciseManager.DEFAULT_EXERCISE_REPETITIONS;
ExerciseManager.TOTAL_EXERCISES_PREFORMED++;
this.selfRotationVectorPaths[0] = new ArrayList<>();
this.selfRotationVectorPaths[1] = new ArrayList<>();
this.repetitionsRemaining = ExerciseManager.DEFAULT_EXERCISE_REPETITIONS;
this.targetRotationVectorPaths = new Vector3f[2][exercise.rightPath.getAngleVectors().length];
this.targetRotationVectorPaths[0] = exercise.leftPath.getAngleVectors();
this.targetRotationVectorPaths[1] = exercise.rightPath.getAngleVectors();
@@ -96,13 +125,28 @@ public class InputProcessor {
this.lastTime = System.currentTimeMillis();
}
/**
* Method that is called whenever the user performs a good repetition.
*/
public void onAdequateRepetition() {
ExerciseManager.TOTAL_REPETITIONS_PERFORMED++;
// TODO: Play sound
}
/**
* Method that is called whenever the user performs a bad repetition.
*/
public void onInadequateRepetition() {
}
/**
* Function for setting whether the motion data
* should be recorded or not.
*
* @param recording Whether the motion data should be recorded.
* @param duration For how long the motion data should be recorded.
* This only has an effect if `recording` is true.
* @param duration For how long the motion data should be recorded.
* This only has an effect if `recording` is true.
*/
public void setRecording(boolean recording, float duration) {
this.recordingMovement = recording;
@@ -171,7 +215,7 @@ public class InputProcessor {
try {
Log.i("MotionProcessor", "Time passed: " + this.secondsPassed + "s");
if ( this.recordingMovement)
if (this.recordingMovement)
Log.i("MotionProcessor", this.secondsPassed + " / " + this.recordingDurationInSeconds);
Log.i("MotionProcessor", "Received packet data: " + data);
@@ -218,20 +262,34 @@ public class InputProcessor {
// Supposed index of the current rotation vector in the `rotationVectorPaths` array
this.selfRotationVectorPaths[deviceId].add(rotation);
if ( this.recordingMovement && this.secondsPassed >= this.recordingDurationInSeconds) {
if (this.recordingMovement && this.secondsPassed >= this.recordingDurationInSeconds) {
// Do something with the recorded data.
this.recordingMovement = false;
// Convert recorded data from `selfRotationVectorPaths` to string, and
// publish to database, or do something else with it.
String converted = convertRecordedDataToString();
String converted = convertRecordedDataToString();
// Do something with it
// Do something with it
Log.i("MotionProcessor", "Converted data: ");
Log.i("MotionProcessor", converted);
}
motionDataConsumer.accept(rotation, deviceId);
// Do something else with the vector
// TODO: Implement !!
Log.i("MotionProcessor", "Rotation vector: " + rotation.toString() + " from device: " + deviceId);
// Whenever the exercise has finished and it's not recording,
// attempt to move to the next exercise.
// If this fails, navigate back to the main activity.
if (this.hasFinished() && !this.recordingMovement) {
this.parentActivity.fetchExerciseAsync(this::nextExercise, (nil) -> {
Log.i("MotionProcessor", "Failed to fetch exercise data.");
NavigationManager.navigateToActivity(this.parentActivity, MainActivity.class);
});
}
}
}
@@ -242,8 +300,7 @@ public class InputProcessor {
*
* @return The converted string.
*/
private String convertRecordedDataToString()
{
private String convertRecordedDataToString() {
int[] intBits = new int[3];
char[] vectorChars = new char[12]; // 4 bytes per scalar, 12 chars per vector
JsonArray jsonArray = new JsonArray();
@@ -255,14 +312,14 @@ public class InputProcessor {
*/
// Iterate over all devices. In the current instance, it's 2.
for ( int deviceId = 0; deviceId < selfRotationVectorPaths.length; deviceId++) {
for (int deviceId = 0; deviceId < selfRotationVectorPaths.length; deviceId++) {
JsonObject jsonDeviceObject = new JsonObject();
jsonDeviceObject.addProperty("deviceId", deviceId);
// Data array
JsonArray jsonDeviceDataArray = new JsonArray();
for ( Vector3f vector : selfRotationVectorPaths[deviceId]) {
for (Vector3f vector : selfRotationVectorPaths[deviceId]) {
JsonArray jsonScalarArray = new JsonArray();
jsonScalarArray.add(vector.x);
jsonScalarArray.add(vector.y);
@@ -286,22 +343,12 @@ public class InputProcessor {
return secondsPassed / exerciseDurationInSeconds;
}
/**
* 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 combined (average) error value of both sensors.
public double getCombinedError()
{
public double getCombinedError()
{
}*/
}*/
/**
* Function for getting the error offsets of the user's path compared to the