Removed FitnessCycle, reformatted code, made Pepper actions globally accessible

This commit is contained in:
Luca Warmenhoven
2024-05-30 17:16:09 +02:00
parent b4ce40ca7d
commit 3f37f44f22
22 changed files with 234 additions and 428 deletions

View File

@@ -19,11 +19,10 @@ public enum EMuscleGroup {
return this.muscleGroupIdentifier;
}
public static EMuscleGroup parse(String name)
{
for ( EMuscleGroup muscleGroup : EMuscleGroup.values())
for ( String muscleGroupName : muscleGroup.muscleGroupNames)
if ( muscleGroupName.equalsIgnoreCase(name))
public static EMuscleGroup parse(String name) {
for (EMuscleGroup muscleGroup : EMuscleGroup.values())
for (String muscleGroupName : muscleGroup.muscleGroupNames)
if (muscleGroupName.equalsIgnoreCase(name))
return muscleGroup;
return null;
}

View File

@@ -1,12 +1,6 @@
package com.example.fitbot.exercise;
import android.util.Log;
import com.example.fitbot.util.path.GesturePath;
import com.example.fitbot.util.server.IWebServerHandler;
import com.example.fitbot.util.server.WebServer;
import java.util.Objects;
public class Exercise {
@@ -22,13 +16,13 @@ public class Exercise {
/**
* Constructor for the AbstractExercise class.
*
* @param muscleGroup The muscle group of the exercise.
* @param leftPath The path of the left hand.
* @param rightPath The path of the right hand.
* @param title The title of the exercise.
* @param description The description of the exercise.
* @param imageUrl The URL of the image.
* @param videoUrl The URL of the video.
* @param muscleGroup The muscle group of the exercise.
* @param leftPath The path of the left hand.
* @param rightPath The path of the right hand.
* @param title The title of the exercise.
* @param description The description of the exercise.
* @param imageUrl The URL of the image.
* @param videoUrl The URL of the video.
*/
public Exercise(EMuscleGroup muscleGroup, String title, String description, String imageUrl, String videoUrl, GesturePath leftPath, GesturePath rightPath, float exerciseTimeInSeconds) {
this.muscleGroup = muscleGroup;

View File

@@ -18,15 +18,15 @@ public class ExerciseManager {
// 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_EXERCISE_DURATION = "duration";
private static final String PROPERTY_EXERCISE_ID = "exerciseId";
private static final String PROPERTY_MUSCLE_GROUP = "muscleGroup";
private static final String PROPERTY_SHORT_DESC = "shortDescription";
private static final String PROPERTY_IMAGE_URL = "imageUrl";
private static final String PROPERTY_VIDEO_URL = "videoUrl";
private static final String PROPERTY_DESC = "description";
private static final String PROPERTY_PATH = "path";
private static final String PROPERTY_NAME = "name";
private static final String PROPERTY_EXERCISE_DURATION = "duration";
private static final String PROPERTY_EXERCISE_ID = "exerciseId";
private static final String PROPERTY_MUSCLE_GROUP = "muscleGroup";
private static final String PROPERTY_SHORT_DESC = "shortDescription";
private static final String PROPERTY_IMAGE_URL = "imageUrl";
private static final String PROPERTY_VIDEO_URL = "videoUrl";
private static final String PROPERTY_DESC = "description";
private static final String PROPERTY_PATH = "path";
private static final String PROPERTY_NAME = "name";
public static final int SENSOR_COUNT = 2;
@@ -37,18 +37,17 @@ public class ExerciseManager {
PROPERTY_SHORT_DESC
};
public static final int DEFAULT_EXERCISE_REPETITIONS = 10;
public static final int DEFAULT_EXERCISE_REPETITIONS = 10;
public static final float DEFAULT_SEGMENT_SPEED = 1.0f;
public static final float EXERCISE_ERROR_MARGIN = 1.0f;
/**
* Function for sending an HTTP request to the server.
*
* @param url The URL to send the request to.
* @param method The method to use for the request, e.g. GET or POST.
* @param url The URL to send the request to.
* @param method The method to use for the request, e.g. GET or POST.
* @param contentType The content type of the request.
* @param body The body of the request.
*
* @param body The body of the request.
* @return The response from the server.
*/
public static String sendHTTP(String url, String method, String contentType, String body) {
@@ -61,7 +60,7 @@ public class ExerciseManager {
connection.connect();
// Send a body if it is present
if (body != null)
connection.getOutputStream().write(body.getBytes());
connection.getOutputStream().write(body.getBytes());
InputStream stream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
StringBuilder builder = new StringBuilder();
@@ -102,7 +101,7 @@ public class ExerciseManager {
// class to support more paths.
String[] leftRightData = content.get(PROPERTY_PATH).getAsString().split(";");
if ( leftRightData.length != SENSOR_COUNT)
if (leftRightData.length != SENSOR_COUNT)
return null;
return new Exercise(

View File

@@ -1,53 +0,0 @@
package com.example.fitbot.exercise;
/**
* The ExerciseUser class represents a user of the exercise application.
* This contains all necessary information of the current user.
*/
public class ExerciseUser {
public float upperArmLength;
public float lowerArmLength;
public float upperLegLength;
public float lowerLegLength;
public float height;
/**
* Constructor for the ExerciseUser class.
* @param upperArmLength The length of the upper arm.
* @param lowerArmLength The length of the lower arm.
* @param height The height of the user.
* @param upperLegLength The length of the upper leg.
* @param lowerLegLength The length of the lower leg.
*/
public ExerciseUser(float upperArmLength, float lowerArmLength, float height, float upperLegLength, float lowerLegLength) {
this.upperArmLength = upperArmLength;
this.lowerArmLength = lowerArmLength;
this.upperLegLength = upperLegLength;
this.lowerLegLength = lowerLegLength;
this.height = height;
}
/**
* Constructor for the ExerciseUser class.
* @param height The height of the user.
*/
public ExerciseUser(float height) {
this.height = height;
this.upperArmLength = height * 0.2f;
this.lowerArmLength = height * 0.2f;
this.upperLegLength = height * 0.3f;
this.lowerLegLength = height * 0.3f;
}
/**
* Default constructor for the ExerciseUser class.
* This sets the default height to 180.0f. (1.80m)
*/
public ExerciseUser() {
this(180.0f);
}
}

View File

@@ -1,15 +1,6 @@
package com.example.fitbot.pepper;
import com.aldebaran.qi.sdk.QiContext;
public abstract class AbstractPepperActionEvent {
public final QiContext context;
public AbstractPepperActionEvent(QiContext context)
{
this.context = context;
}
public abstract EPepperAction getAction();
}

View File

@@ -6,5 +6,5 @@ package com.example.fitbot.pepper;
public enum EPepperAction {
ACTION_SPEAK,
ACTION_ANIMATE,
NO_ACTION;
NO_ACTION
}

View File

@@ -1,6 +1,7 @@
package com.example.fitbot.pepper;
import com.aldebaran.qi.sdk.QiContext;
import com.aldebaran.qi.sdk.RobotLifecycleCallbacks;
import com.aldebaran.qi.sdk.object.locale.Language;
import com.aldebaran.qi.sdk.object.locale.Locale;
import com.aldebaran.qi.sdk.object.locale.Region;
@@ -11,37 +12,50 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class representing the Pepper robot.
* All functionality related to Pepper is implemented in this class.
* <p>
* Before any of the functions in this class can be used, the Pepper class needs a QiContext to be
* set. This is retrieved in the `onRobotFocusGained` method of the RobotLifecycleCallbacks
* interface, and can then provided using the `provideContext` method.
* </p>
*/
public class Pepper {
// Queue containing all Pepper actions that need to be executed
// This is to prevent the app from crashing when attempting to perform multiple actions at once
private static final ConcurrentLinkedQueue<AbstractPepperActionEvent> pepperActionEventQueue =
new ConcurrentLinkedQueue<>();
private static final AtomicBoolean isAnimating = new AtomicBoolean(false);
private static final AtomicBoolean isSpeaking = new AtomicBoolean(false);
private static final AtomicBoolean isAnimating = new AtomicBoolean(false);
private static final AtomicBoolean isSpeaking = new AtomicBoolean(false);
private static final Locale DEFAULT_LOCALE = new Locale(Language.DUTCH, Region.NETHERLANDS);
/**
* Speak a phrase with Pepper.
* The latest QiContext used by Pepper.
* This can be publicly accessed to get the latest QiContext used by Pepper.
*/
public static QiContext latestContext = null;
/**
* Make the Pepper robot speak a phrase.
* This function adds a SpeechEvent to the event queue,
* and will speak the phrase once Pepper is ready to process it.
*
* @param phrase The phrase to speak
* @param context The QiContext to use
*/
public static void speak(String phrase, QiContext context)
{
addToEventQueue(new PepperSpeechEvent(context, phrase, DEFAULT_LOCALE));
public static void speak(String phrase) {
addToEventQueue(new PepperSpeechEvent(phrase, DEFAULT_LOCALE));
}
/**
* Animate Pepper with the given animation.
* Make the Pepper robot perform an animation.
* This function adds an AnimationEvent to the event queue,
* and will perform the animation once Pepper is ready to process it.
*
* @param animationName The name of the animation to play
* @param context The QiContext to use
*/
public static void animate(String animationName, QiContext context)
{
addToEventQueue(new PepperAnimationEvent(context, animationName));
public static void animate(String animationName) {
addToEventQueue(new PepperAnimationEvent(animationName));
}
/**
@@ -50,8 +64,7 @@ public class Pepper {
*
* @param event The event to add to the queue
*/
public static void addToEventQueue(AbstractPepperActionEvent event)
{
public static void addToEventQueue(AbstractPepperActionEvent event) {
pepperActionEventQueue.add(event);
processEventQueue();
}
@@ -63,25 +76,23 @@ public class Pepper {
* This prevents multiple actions from being executed at the same time, which can
* cause the application to crash.
*/
private static synchronized void processEventQueue()
{
if (!pepperActionEventQueue.isEmpty())
{
private static synchronized void processEventQueue() {
if (!pepperActionEventQueue.isEmpty()) {
AbstractPepperActionEvent event = pepperActionEventQueue.poll();
if (event == null)
// Prevent null pointer exceptions
if (event == null || latestContext == null)
return;
switch (event.getAction())
{
switch (event.getAction()) {
/* Event for speaking a phrase **/
case ACTION_SPEAK:
if ( !(event instanceof PepperSpeechEvent) || isSpeaking.get())
if (!(event instanceof PepperSpeechEvent) || isSpeaking.get())
break;
PepperSpeechEvent speechEvent = (PepperSpeechEvent) event;
isSpeaking.set(true);
speechEvent
.sayPhrase()
.getSay(latestContext)
.async()
.run()
.andThenConsume(future -> {
@@ -90,13 +101,13 @@ public class Pepper {
});
break;
/* Event for animating the robot **/
/* Event for animating the robot **/
case ACTION_ANIMATE:
if ( !(event instanceof PepperAnimationEvent) || isAnimating.get())
if (!(event instanceof PepperAnimationEvent) || isAnimating.get())
break;
PepperAnimationEvent animationEvent = (PepperAnimationEvent) event;
animationEvent
.getAnimation()
.getAnimation(latestContext)
.async()
.run()
.andThenConsume(future -> {
@@ -105,9 +116,27 @@ public class Pepper {
});
break;
default:
// Do nothing
break;
}
}
}
/**
* Function for providing the latest QiContext to Pepper.
*
* @param context The QiContext to use.
* This can be null if the context is not available.
* @param origin The origin of the context.
* This parameter is to prevent other classes from
* unnecessarily providing a context, or setting
* the context to null.
*/
public static void provideContext(QiContext context, Class<? extends RobotLifecycleCallbacks> origin) {
latestContext = context;
if (context != null) {
processEventQueue();
}
}
}

View File

@@ -9,10 +9,27 @@ import com.aldebaran.qi.sdk.object.actuation.Animation;
public class PepperAnimationEvent extends AbstractPepperActionEvent {
public final String animationName;
private final IAnimationCompletedListener onLabelReachedListener;
public PepperAnimationEvent(QiContext context, String animationName) {
super(context);
/**
* Constructor for PepperAnimationEvent
*
* @param animationName The name of the animation to play
* @param onLabelReachedListener The listener to call when the animation is completed
*/
public PepperAnimationEvent(String animationName, IAnimationCompletedListener onLabelReachedListener) {
this.animationName = animationName;
this.onLabelReachedListener = onLabelReachedListener;
}
/**
* Constructor for PepperAnimationEvent
*
* @param animationName The name of the animation to play
*/
public PepperAnimationEvent(String animationName) {
this(animationName, null);
}
@Override
@@ -24,14 +41,25 @@ public class PepperAnimationEvent extends AbstractPepperActionEvent {
* Returns an animation object, which can be used to play the animation
* in the Pepper class.
*/
public Animate getAnimation()
{
Animation animation = AnimationBuilder.with(this.context)
.withResources(this.context.getResources().getIdentifier(animationName, "raw", this.context.getPackageName()))
public Animate getAnimation(QiContext context) {
Animation animation = AnimationBuilder.with(context)
.withResources(context.getResources().getIdentifier(animationName, "raw", context.getPackageName()))
.build();
return AnimateBuilder.with(this.context)
Animate animate = AnimateBuilder.with(context)
.withAnimation(animation)
.build();
// Add a listener for when a label is reached
animate.addOnLabelReachedListener((label, time) -> {
if (onLabelReachedListener != null && "end".equals(label)) {
onLabelReachedListener.onComplete();
}
});
return animate;
}
public interface IAnimationCompletedListener {
void onComplete();
}
}

View File

@@ -10,9 +10,7 @@ public class PepperSpeechEvent extends AbstractPepperActionEvent {
public final String phrase;
public final Locale locale;
public PepperSpeechEvent(QiContext context, String phrase, Locale locale)
{
super(context);
public PepperSpeechEvent(String phrase, Locale locale) {
this.locale = locale;
this.phrase = phrase;
}
@@ -25,9 +23,8 @@ public class PepperSpeechEvent extends AbstractPepperActionEvent {
/**
* Returns a Say object, which can then be executed.
*/
public Say sayPhrase()
{
return SayBuilder.with(this.context)
public Say getSay(QiContext context) {
return SayBuilder.with(context)
.withText(this.phrase)
.withLocale(this.locale)
.build();

View File

@@ -1,6 +1,9 @@
package com.example.fitbot.ui.activities;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.VideoView;
import com.aldebaran.qi.sdk.QiContext;
@@ -12,9 +15,8 @@ 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.PersonalMotionPreviewElement;
import com.example.fitbot.ui.components.ExerciseStatusElement;
import com.example.fitbot.util.NavigationManager;
import com.example.fitbot.util.FitnessCycle;
import com.example.fitbot.util.processing.InputProcessor;
import org.joml.Vector3f;
@@ -22,14 +24,12 @@ import org.joml.Vector3f;
public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks {
// Private fields for the FitnessActivity class.
private PersonalMotionPreviewElement personalMotionPreviewElement;
private ExerciseStatusElement 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[] {
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.",
", de oefeningen slapen nog, probeer het later nog eens."
@@ -39,7 +39,7 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
"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 int EXERCISE_COUNT = 5;
private static final float EXERCISE_SPEED_MULTIPLIER = 1.0f;
@Override
@@ -54,17 +54,16 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
// Find the VideoView by its ID
VideoView videoView = findViewById(R.id.videoView);
FitnessCycle.playVideo(videoView, this);
playVideo(videoView, this);
NavigationManager.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class);
NavigationManager.hideSystemUI(this);
}
@Override
public void onRobotFocusGained(QiContext qiContext) {
this.qiContext = qiContext;
// Find the VideoView by its ID
// CompletableFuture.runAsync(() -> FitnessCycle.executeMovement("bicepcurl", 10, qiContext));
// Provide the context so that all queued actions can be performed.
Pepper.provideContext(qiContext, this.getClass());
personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement);
@@ -74,19 +73,18 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
personalMotionPreviewElement.post(() -> {
this.acquireExercise((exercise) -> {
// Acquire paths from the exercise and provide them to the motion processor
Vector3f[][] vectors = new Vector3f[][] {exercise.leftPath.getVectors(), exercise.rightPath.getVectors()};
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);
}, (n) -> {
int randomMessageIndex = (int)Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length);
Pepper.speak(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex], qiContext);
Pepper.speak(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE, qiContext);
int randomMessageIndex = (int) Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length);
Pepper.speak(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex]);
Pepper.speak(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE);
NavigationManager.navigateToActivity(this, MainActivity.class);
});
});
@@ -104,33 +102,59 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
// therefore we'll have to do it like this...
(new Thread(() -> {
Exercise exercise = ExerciseManager.retrieveExercise();
if ( exercise == null)
{
if (exercise == null) {
onFailedFetch.handle(null);
}
else {
} else {
onSuccessfulFetch.handle(exercise);
}
})).start();
}
/**
* Function for playing a video in a VideoView
*
* @param videoView The VideoView to play the video in
* @param context The context to use
*/
public static void playVideo(VideoView videoView, Context context) {
// Set up the video player
if (videoView != null) {
Uri videoUri = Uri.parse("android.resource://" + context.getPackageName() + "/" + R.raw.bicepvideo);
videoView.setVideoURI(videoUri);
videoView.setOnCompletionListener(mp -> {
// Repeat the video when it finishes playing
videoView.start();
});
videoView.start();
} else {
Log.e("FitnessActivity", "VideoView is null. Check your layout XML.");
}
}
@Override
public void onRobotFocusLost() {
QiSDK.unregister(this, this);
Pepper.provideContext(null, this.getClass()); // Remove the context (unavailable)
// Go to the main screen
NavigationManager.navigateToActivity(this, MainActivity.class);
}
@Override
public void onRobotFocusRefused(String reason) {}
public void onRobotFocusRefused(String reason) {
}
@Override
protected void onDestroy() {
if ( this.motionProcessor != null ) {
if (this.motionProcessor != null) {
this.motionProcessor.stopListening();
this.motionProcessor = null;
}
QiSDK.unregister(this, this);
Pepper.provideContext(null, this.getClass());
super.onDestroy();
}
}

View File

@@ -1,9 +1,7 @@
package com.example.fitbot.ui.activities;
import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.support.v7.app.AppCompatActivity;
import com.example.fitbot.R;
import com.example.fitbot.util.NavigationManager;

View File

@@ -9,13 +9,12 @@ import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.aldebaran.qi.sdk.QiContext;
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.FitnessCycle;
import com.example.fitbot.util.processing.IInputHandler;
import com.example.fitbot.util.processing.InputProcessor;
@@ -26,12 +25,11 @@ import org.joml.Vector4f;
import java.util.concurrent.ConcurrentLinkedQueue;
public class PersonalMotionPreviewElement extends View implements IInputHandler {
public class ExerciseStatusElement extends View implements IInputHandler {
// Fields regarding Exercise and speech handling.
private InputProcessor motionProcessor;
private Exercise exercise;
private QiContext qiContext;
private int exerciseCount;
private FitnessActivity parentActivity;
@@ -41,10 +39,10 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
private final Paint backgroundPaint = new Paint();
// TODO: Remove
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 final Matrix4f viewMatrix = new Matrix4f(); // The view matrix for the 3D to 2D transformation.
private final 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 ConcurrentLinkedQueue<Vector2f> vectors = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<Vector2f> vectors = new ConcurrentLinkedQueue<>();
// TODO: Remove
private Vector2f[] axisVectors = new Vector2f[0];
@@ -57,11 +55,10 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
};
public PersonalMotionPreviewElement(Context context, AttributeSet attrs) {
public ExerciseStatusElement(Context context, AttributeSet attrs) {
super(context, attrs);
if ( context instanceof Activity)
{
if (context instanceof Activity) {
this.parentActivity = (FitnessActivity) context;
}
@@ -85,9 +82,9 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
* 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 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.
* @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.");
@@ -96,8 +93,8 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
this.exercise = exercise;
this.exerciseCount = exerciseCount;
// TODO: Remove
this.axisVectors = new Vector2f[] {
/* TODO: Remove */
this.axisVectors = new Vector2f[]{
projectVertex(new Vector3f(-5.0f, 0, 0), getWidth(), getHeight()),
projectVertex(new Vector3f(5.0f, 0, 0), getWidth(), getHeight()),
projectVertex(new Vector3f(0, -5.0f, 0), getWidth(), getHeight()),
@@ -106,25 +103,24 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
projectVertex(new Vector3f(0, 0, 5.0f), getWidth(), getHeight())
};
Pepper.speak(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)]);
// Handler that is called every time the motion processor receives new data.
this.motionProcessor.setInputHandler((rotationVector, deviceId) -> {
Log.i("MotionProcessor", "Rotation vector received: " + rotationVector);
Log.i("MotionProcessor", "Last error offset:" + this.motionProcessor.getError(deviceId, this.motionProcessor.secondsPassed()));
if ( this.motionProcessor.hasFinished())
{
if (this.motionProcessor.hasFinished()) {
// If for some reason the parent activity is not defined,
// move back to the main screen.
if ( this.parentActivity == null)
{
if (this.parentActivity == null) {
// Move to main screen
NavigationManager.navigateToActivity(getContext(), MainActivity.class);
return;
}
// Move on to the next exercise, or finish.
if ( this.exerciseCount > 0 )
{
if (this.exerciseCount > 0) {
this.exerciseCount--;
this.parentActivity.acquireExercise((newExercise) -> {
this.motionProcessor.useExercise(newExercise);
@@ -132,43 +128,27 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
// Move to main screen
NavigationManager.navigateToActivity(parentActivity, MainActivity.class);
});
}
else
{
} else {
// Finish the exercise.
NavigationManager.navigateToActivity(parentActivity, EndScreenActivity.class);
return;
}
}
// TODO: Adjust / remove
/* TODO: Use raw vector */
vectors.add(projectVertex(rotationVector, this.getWidth(), this.getHeight()));
Log.i("MotionProcessor", "Rotation vector received: " + rotationVector);
/* TODO: Remove */
Vector2f parsed = projectVertex(rotationVector, this.getWidth(), this.getHeight());
this.vectors.add(parsed);
this.vectors.add(parsed /* TODO: Add regular vertex once exercise data is stored in DB*/);
// Remove the first element if the array is too big
if (this.vectors.size() > 100)
this.vectors.poll();
});
}
/**
* Function for providing a QiContext to the PersonalMotionPreviewElement.
* This function will be called by the parent activity when the QiContext is available.
* Also say something nice to the user :)
*
* @param context The QiContext to provide.
*/
public void provideQiContext(QiContext context) {
this.qiContext = context;
if (this.qiContext == null)
return;
FitnessCycle.say(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)], this.qiContext);
}
/**
* Method for setting the gesture path that will be drawn on the canvas.
*
@@ -203,7 +183,6 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint);
@@ -211,17 +190,17 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
/*if (this.exercise == null)
return;*/
for (int i = 0, startX, endX, startY, endY; i < axisVectors.length/2; i++)
{
startX = (int)Math.max(0, Math.min(this.getWidth(), (int)axisVectors[i*2].x));
endX = (int)Math.max(0, Math.min(this.getWidth(), (int)axisVectors[i*2+1].x));
startY = (int)Math.max(0, Math.min(this.getHeight(), (int)axisVectors[i*2].y));
endY = (int)Math.max(0, Math.min(this.getHeight(), (int)axisVectors[i*2+1].y));
/* TODO: Remove */
for (int i = 0, startX, endX, startY, endY; i < axisVectors.length / 2; i++) {
startX = (int) Math.max(0, Math.min(this.getWidth(), (int) axisVectors[i * 2].x));
endX = (int) Math.max(0, Math.min(this.getWidth(), (int) axisVectors[i * 2 + 1].x));
startY = (int) Math.max(0, Math.min(this.getHeight(), (int) axisVectors[i * 2].y));
endY = (int) Math.max(0, Math.min(this.getHeight(), (int) axisVectors[i * 2 + 1].y));
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.userProgressPaint);
}
/*

View File

@@ -11,8 +11,7 @@ import com.aldebaran.qi.sdk.object.actuation.Animation;
public class Animations {
public static void Animate(String AnimationFile, QiContext ctx)
{
public static void Animate(String AnimationFile, QiContext ctx) {
int resId = ctx.getResources().getIdentifier(AnimationFile, "raw", ctx.getPackageName());
Animation animation = AnimationBuilder.with(ctx)

View File

@@ -1,164 +0,0 @@
package com.example.fitbot.util;
import android.content.Context;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.VideoView;
import com.aldebaran.qi.sdk.builder.SayBuilder;
import com.aldebaran.qi.sdk.object.conversation.Phrase;
import com.aldebaran.qi.sdk.object.conversation.Say;
import com.aldebaran.qi.sdk.object.locale.Language;
import com.aldebaran.qi.sdk.object.locale.Locale;
import com.aldebaran.qi.sdk.object.locale.Region;
import com.example.fitbot.R;
import com.aldebaran.qi.sdk.QiContext;
import com.aldebaran.qi.sdk.builder.AnimateBuilder;
import com.aldebaran.qi.sdk.builder.AnimationBuilder;
import com.aldebaran.qi.sdk.object.actuation.Animate;
import com.aldebaran.qi.sdk.object.actuation.Animation;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class FitnessCycle extends AppCompatActivity {
private static final Locale DUTCH_LOCALE = new Locale(Language.DUTCH, Region.NETHERLANDS);
private static final BlockingQueue<PepperSpeechEntry> MESSAGE_QUEUE = new LinkedBlockingQueue<>();
private static boolean SPEECH_ACTIVE = false;
/**
* Function for executing a movement animation a certain number of times
* on the robot
*
* @param Exercise The name of the exercise to perform
* @param Reps The number of repetitions to perform
* @param qiContext The QiContext to use
*/
public static void executeMovement(String Exercise, int Reps, QiContext qiContext) {
AtomicInteger repCount = new AtomicInteger(0);
Animation animation = AnimationBuilder.with(qiContext)
.withResources(qiContext.getResources().getIdentifier(Exercise, "raw", qiContext.getPackageName()))
.build();
Animate animate = AnimateBuilder.with(qiContext)
.withAnimation(animation)
.build();
// Add a listener for when a label is reached
animate.addOnLabelReachedListener((label, time) -> {
// Increment repCount when the end of a repetition is reached
if ("end".equals(label)) {
repCount.incrementAndGet();
}
});
// Run the animation the desired number of times
for (int i = 0; i < Reps; i++) {
animate.run();
}
}
/**
* Function for making the robot say something with DUTCH_LOCALE as locale
*
* @param phrase The phrase to make the robot say
* @param ctx The QiContext to use
*/
public static void say(String phrase, QiContext ctx) {
say(phrase, ctx, DUTCH_LOCALE);
}
/**
* Function for making the robot say something with a specific locale
*
* @param phrase The phrase to make the robot say
* @param ctx The QiContext to use
* @param locale The locale to use
*/
public static void say(String phrase, QiContext ctx, Locale locale) {
// If the robot is already speaking, add the phrase to the queue
MESSAGE_QUEUE.add(new PepperSpeechEntry(phrase, ctx, locale));
processMessageQueue();
}
private static synchronized void processMessageQueue() {
if (SPEECH_ACTIVE)
return;
PepperSpeechEntry nextPhrase = MESSAGE_QUEUE.poll();
if (nextPhrase != null) {
SPEECH_ACTIVE = true;
Say say = nextPhrase.getSay();
say.async().run().andThenConsume(future -> {
SPEECH_ACTIVE = false;
processMessageQueue(); // Process next item in the queue
});
}
}
/**
* Function for playing a video in a VideoView
*
* @param videoView The VideoView to play the video in
* @param context The context to use
*/
public static void playVideo(VideoView videoView, Context context) {
// Set up the video player
if (videoView != null) {
Uri videoUri = Uri.parse("android.resource://" + context.getPackageName() + "/" + R.raw.bicepvideo);
videoView.setVideoURI(videoUri);
videoView.setOnCompletionListener(mp -> {
// Repeat the video when it finishes playing
videoView.start();
});
videoView.start();
} else {
Log.e("FitnessActivity", "VideoView is null. Check your layout XML.");
}
}
private static class PepperAnimateEvent extends PepperActionEvent {
}
private static class PepperSpeechEntry extends PepperActionEvent {
public String phrase;
public QiContext context;
public Locale locale;
public PepperSpeechEntry(String phrase, QiContext context, Locale locale) {
this.action = PepperAction.SPEAK;
this.data = this;
this.phrase = phrase;
this.context = context;
this.locale = locale;
}
public Say getSay() {
return SayBuilder
.with(context)
.withLocale(locale)
.withText(phrase)
.build();
}
}
private enum PepperAction {
SPEAK,
ANIMATE,
MOVE
}
private static class PepperActionEvent {
public PepperAction action;
public Object data;
}
}

View File

@@ -6,23 +6,18 @@ import android.content.Intent;
import android.view.View;
import android.widget.Button;
import com.example.fitbot.ui.activities.FitnessActivity;
import com.example.fitbot.ui.activities.MainActivity;
import com.example.fitbot.ui.activities.HelpActivity;
public class NavigationManager {
/**
* Sets up a button to navigate to a different activity when clicked.
*
* @param currentActivity The activity that contains the button
* @param buttonId The ID of the button
* @param targetActivity The activity to navigate to
* @param buttonId The ID of the button
* @param targetActivity The activity to navigate to
*/
public static void setupButtonNavigation(Activity currentActivity, int buttonId, Class<? extends Activity> targetActivity) {
Button button = currentActivity.findViewById(buttonId);
if ( button == null )
{
if (button == null) {
throw new IllegalArgumentException("Button with ID " + buttonId + " not found in " + currentActivity.getClass().getSimpleName());
}
button.setOnClickListener(v -> NavigationManager.navigateToActivity(currentActivity, targetActivity));
@@ -32,7 +27,7 @@ public class NavigationManager {
* Navigates to the target activity.
*
* @param currentActivity The current activity
* @param targetActivity The target activity
* @param targetActivity The target activity
*/
public static void navigateToActivity(Activity currentActivity, Class<? extends Activity> targetActivity) {
Intent intent = new Intent(currentActivity, targetActivity);
@@ -45,7 +40,7 @@ public class NavigationManager {
/**
* Navigates to the target activity.
*
* @param context The context
* @param context The context
* @param targetActivity The target activity
*/
public static void navigateToActivity(Context context, Class<? extends Activity> targetActivity) {

View File

@@ -2,10 +2,6 @@ package com.example.fitbot.util.path;
import org.joml.Vector3f;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class GesturePath {
// The vectors that make up the path.
@@ -16,18 +12,18 @@ public class GesturePath {
*
* @param vectors The vectors that make up the path.
*/
public GesturePath(Vector3f[] vectors)
{
if ( vectors.length < 2)
public GesturePath(Vector3f[] vectors) {
if (vectors.length < 2)
throw new IllegalArgumentException("A path must have at least two points.");
this.segments = new PathSegment[vectors.length - 1];
for ( int i = 0; i < vectors.length - 1; i++)
for (int i = 0; i < vectors.length - 1; i++)
segments[i] = new PathSegment(vectors[i], vectors[i + 1]);
}
/**
* Constructor for a GesturePath with provided PathSegments.
*
* @param segments The PathSegments to initialize the path with.
*/
public GesturePath(PathSegment... segments) {
@@ -46,11 +42,10 @@ public class GesturePath {
/**
* Method for retrieving the vectors of the GesturePath.
*/
public Vector3f[] getVectors()
{
public Vector3f[] getVectors() {
Vector3f[] vectors = new Vector3f[segments.length + 1];
vectors[0] = segments[0].getStart();
for ( int i = 0; i < segments.length; i++)
for (int i = 0; i < segments.length; i++)
vectors[i + 1] = segments[i].getEnd();
return vectors;
@@ -64,11 +59,11 @@ public class GesturePath {
*/
public PathSegment closest(Vector3f reference) {
// If there's only one segment, return that one.
if ( segments.length == 1)
if (segments.length == 1)
return segments[0];
PathSegment closest = segments[0];
for ( int i = 1; i < segments.length; i++)
for (int i = 1; i < segments.length; i++)
closest = PathSegment.closer(closest, segments[i], reference);
return closest;
@@ -89,7 +84,7 @@ public class GesturePath {
* 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.
*
* <p>
* Note: ASCII to Vector conversion is done in Big Endian format (most significant byte first).
*
* @param input The string to convert

View File

@@ -12,7 +12,7 @@ public class PathSegment {
* pointing straight upwards relative to the line, with a curvature of 0.0.
*
* @param start The starting point of the line segment
* @param end The end point of the line segment.
* @param end The end point of the line segment.
*/
public PathSegment(Vector3f start, Vector3f end) {
this.start = start;
@@ -25,7 +25,7 @@ public class PathSegment {
* depending on the curvature of the curve. If the curvature is unset, or set to 0
* then this method will use linear interpolation. Otherwise, it will use a curve.
*
* @param t The interpolation value between 0 and 1.
* @param t The interpolation value between 0 and 1.
*/
public Vector3f interpolate(double t) {
return new Vector3f(this.start)
@@ -56,7 +56,7 @@ public class PathSegment {
(float) (this.start.x + t * (this.end.x - this.start.x)),
(float) (this.start.y + t * (this.end.y - this.start.y)),
(float) (this.start.z + t * (this.end.z - this.start.z))
));
));
}
/**
@@ -84,7 +84,7 @@ public class PathSegment {
* @return The distance to the closest point on the path segment.
*/
public double distance(Vector3f reference) {
if ( this.start.distanceSquared(reference) > this.end.distanceSquared(reference))
if (this.start.distanceSquared(reference) > this.end.distanceSquared(reference))
return this.end.distance(reference);
return this.start.distance(reference);
}
@@ -92,8 +92,8 @@ public class PathSegment {
/**
* Function for returning the closest path segment to a reference point.
*
* @param first The first path segment to compare.
* @param second The second path segment to compare.
* @param first The first path segment to compare.
* @param second The second path segment to compare.
* @param referencePoint The reference point to compare to.
* @return The closest path segment to the reference point.
*/

View File

@@ -4,11 +4,12 @@ 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);
/**
* 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);
}

View File

@@ -49,6 +49,7 @@ public class InputProcessor {
* 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) {
@@ -61,6 +62,7 @@ public class InputProcessor {
/**
* Function for checking if the exercise has finished.
*
* @return True if the exercise has finished, false otherwise.
*/
public boolean hasFinished() {
@@ -171,8 +173,7 @@ public class InputProcessor {
*
* @return The current progress of the exercise.
*/
public double getCurrentProgress()
{
public double getCurrentProgress() {
return secondsPassed / exerciseDuration;
}
@@ -208,8 +209,7 @@ public class InputProcessor {
// 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)
{
selfIndex >= 0 && selfIndex <= this.selfRotationVectorPaths[sensorId].length - 1) {
return this.selfRotationVectorPaths[sensorId][selfIndex].distance(this.targetRotationVectorPaths[sensorId][targetIndex]);
}
return 0.0d;

View File

@@ -1,7 +1,5 @@
package com.example.fitbot.util.server;
import java.net.Socket;
/**
* Interface for handling WebSocket events.
*/

View File

@@ -10,16 +10,13 @@ import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class WebServer implements Runnable {
private ServerSocket serverSocket;
protected IWebServerHandler eventHandler = (input) -> {}; // No-op.
protected IWebServerHandler eventHandler = (input) -> {
}; // No-op.
private Thread thread;
private final AtomicBoolean forceClose = new AtomicBoolean(false);
@@ -84,7 +81,7 @@ public class WebServer implements Runnable {
String[] data = builder.toString().split("\n\n");
if ( data.length > 1) { // Check if the data is valid.
if (data.length > 1) { // Check if the data is valid.
this.eventHandler.onReceive(data[1]);
}

View File

@@ -56,7 +56,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.example.fitbot.ui.components.PersonalMotionPreviewElement
<com.example.fitbot.ui.components.ExerciseStatusElement
android:id="@+id/personalMotionPreviewElement"
android:layout_width="350dp"
android:layout_height="350dp" />