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; return this.muscleGroupIdentifier;
} }
public static EMuscleGroup parse(String name) public static EMuscleGroup parse(String name) {
{ for (EMuscleGroup muscleGroup : EMuscleGroup.values())
for ( EMuscleGroup muscleGroup : EMuscleGroup.values()) for (String muscleGroupName : muscleGroup.muscleGroupNames)
for ( String muscleGroupName : muscleGroup.muscleGroupNames) if (muscleGroupName.equalsIgnoreCase(name))
if ( muscleGroupName.equalsIgnoreCase(name))
return muscleGroup; return muscleGroup;
return null; return null;
} }

View File

@@ -1,12 +1,6 @@
package com.example.fitbot.exercise; package com.example.fitbot.exercise;
import android.util.Log;
import com.example.fitbot.util.path.GesturePath; 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 { public class Exercise {

View File

@@ -48,7 +48,6 @@ public class ExerciseManager {
* @param method The method to use for the request, e.g. GET or POST. * @param method The method to use for the request, e.g. GET or POST.
* @param contentType The content type of the request. * @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. * @return The response from the server.
*/ */
public static String sendHTTP(String url, String method, String contentType, String body) { public static String sendHTTP(String url, String method, String contentType, String body) {
@@ -102,7 +101,7 @@ public class ExerciseManager {
// class to support more paths. // class to support more paths.
String[] leftRightData = content.get(PROPERTY_PATH).getAsString().split(";"); String[] leftRightData = content.get(PROPERTY_PATH).getAsString().split(";");
if ( leftRightData.length != SENSOR_COUNT) if (leftRightData.length != SENSOR_COUNT)
return null; return null;
return new Exercise( 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; package com.example.fitbot.pepper;
import com.aldebaran.qi.sdk.QiContext;
public abstract class AbstractPepperActionEvent { public abstract class AbstractPepperActionEvent {
public final QiContext context;
public AbstractPepperActionEvent(QiContext context)
{
this.context = context;
}
public abstract EPepperAction getAction(); public abstract EPepperAction getAction();
} }

View File

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

View File

@@ -1,6 +1,7 @@
package com.example.fitbot.pepper; package com.example.fitbot.pepper;
import com.aldebaran.qi.sdk.QiContext; 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.Language;
import com.aldebaran.qi.sdk.object.locale.Locale; import com.aldebaran.qi.sdk.object.locale.Locale;
import com.aldebaran.qi.sdk.object.locale.Region; import com.aldebaran.qi.sdk.object.locale.Region;
@@ -11,9 +12,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Class representing the Pepper robot. * Class representing the Pepper robot.
* All functionality related to Pepper is implemented in this class. * 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 { 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 = private static final ConcurrentLinkedQueue<AbstractPepperActionEvent> pepperActionEventQueue =
new ConcurrentLinkedQueue<>(); new ConcurrentLinkedQueue<>();
@@ -23,25 +31,31 @@ public class Pepper {
private static final Locale DEFAULT_LOCALE = new Locale(Language.DUTCH, Region.NETHERLANDS); 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 phrase The phrase to speak
* @param context The QiContext to use
*/ */
public static void speak(String phrase, QiContext context) public static void speak(String phrase) {
{ addToEventQueue(new PepperSpeechEvent(phrase, DEFAULT_LOCALE));
addToEventQueue(new PepperSpeechEvent(context, 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 animationName The name of the animation to play
* @param context The QiContext to use
*/ */
public static void animate(String animationName, QiContext context) public static void animate(String animationName) {
{ addToEventQueue(new PepperAnimationEvent(animationName));
addToEventQueue(new PepperAnimationEvent(context, animationName));
} }
/** /**
@@ -50,8 +64,7 @@ public class Pepper {
* *
* @param event The event to add to the queue * @param event The event to add to the queue
*/ */
public static void addToEventQueue(AbstractPepperActionEvent event) public static void addToEventQueue(AbstractPepperActionEvent event) {
{
pepperActionEventQueue.add(event); pepperActionEventQueue.add(event);
processEventQueue(); processEventQueue();
} }
@@ -63,25 +76,23 @@ public class Pepper {
* This prevents multiple actions from being executed at the same time, which can * This prevents multiple actions from being executed at the same time, which can
* cause the application to crash. * cause the application to crash.
*/ */
private static synchronized void processEventQueue() private static synchronized void processEventQueue() {
{ if (!pepperActionEventQueue.isEmpty()) {
if (!pepperActionEventQueue.isEmpty())
{
AbstractPepperActionEvent event = pepperActionEventQueue.poll(); AbstractPepperActionEvent event = pepperActionEventQueue.poll();
if (event == null) // Prevent null pointer exceptions
if (event == null || latestContext == null)
return; return;
switch (event.getAction()) switch (event.getAction()) {
{
/* Event for speaking a phrase **/ /* Event for speaking a phrase **/
case ACTION_SPEAK: case ACTION_SPEAK:
if ( !(event instanceof PepperSpeechEvent) || isSpeaking.get()) if (!(event instanceof PepperSpeechEvent) || isSpeaking.get())
break; break;
PepperSpeechEvent speechEvent = (PepperSpeechEvent) event; PepperSpeechEvent speechEvent = (PepperSpeechEvent) event;
isSpeaking.set(true); isSpeaking.set(true);
speechEvent speechEvent
.sayPhrase() .getSay(latestContext)
.async() .async()
.run() .run()
.andThenConsume(future -> { .andThenConsume(future -> {
@@ -92,11 +103,11 @@ public class Pepper {
/* Event for animating the robot **/ /* Event for animating the robot **/
case ACTION_ANIMATE: case ACTION_ANIMATE:
if ( !(event instanceof PepperAnimationEvent) || isAnimating.get()) if (!(event instanceof PepperAnimationEvent) || isAnimating.get())
break; break;
PepperAnimationEvent animationEvent = (PepperAnimationEvent) event; PepperAnimationEvent animationEvent = (PepperAnimationEvent) event;
animationEvent animationEvent
.getAnimation() .getAnimation(latestContext)
.async() .async()
.run() .run()
.andThenConsume(future -> { .andThenConsume(future -> {
@@ -105,9 +116,27 @@ public class Pepper {
}); });
break; break;
default: default:
// Do nothing
break; 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 class PepperAnimationEvent extends AbstractPepperActionEvent {
public final String animationName; 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.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 @Override
@@ -24,14 +41,25 @@ public class PepperAnimationEvent extends AbstractPepperActionEvent {
* Returns an animation object, which can be used to play the animation * Returns an animation object, which can be used to play the animation
* in the Pepper class. * in the Pepper class.
*/ */
public Animate getAnimation() public Animate getAnimation(QiContext context) {
{ Animation animation = AnimationBuilder.with(context)
Animation animation = AnimationBuilder.with(this.context) .withResources(context.getResources().getIdentifier(animationName, "raw", context.getPackageName()))
.withResources(this.context.getResources().getIdentifier(animationName, "raw", this.context.getPackageName()))
.build(); .build();
return AnimateBuilder.with(this.context) Animate animate = AnimateBuilder.with(context)
.withAnimation(animation) .withAnimation(animation)
.build(); .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 String phrase;
public final Locale locale; public final Locale locale;
public PepperSpeechEvent(QiContext context, String phrase, Locale locale) public PepperSpeechEvent(String phrase, Locale locale) {
{
super(context);
this.locale = locale; this.locale = locale;
this.phrase = phrase; this.phrase = phrase;
} }
@@ -25,9 +23,8 @@ public class PepperSpeechEvent extends AbstractPepperActionEvent {
/** /**
* Returns a Say object, which can then be executed. * Returns a Say object, which can then be executed.
*/ */
public Say sayPhrase() public Say getSay(QiContext context) {
{ return SayBuilder.with(context)
return SayBuilder.with(this.context)
.withText(this.phrase) .withText(this.phrase)
.withLocale(this.locale) .withLocale(this.locale)
.build(); .build();

View File

@@ -1,6 +1,9 @@
package com.example.fitbot.ui.activities; package com.example.fitbot.ui.activities;
import android.content.Context;
import android.net.Uri;
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;
@@ -12,9 +15,8 @@ import com.example.fitbot.R;
import com.example.fitbot.exercise.Exercise; import com.example.fitbot.exercise.Exercise;
import com.example.fitbot.exercise.ExerciseManager; import com.example.fitbot.exercise.ExerciseManager;
import com.example.fitbot.pepper.Pepper; 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.NavigationManager;
import com.example.fitbot.util.FitnessCycle;
import com.example.fitbot.util.processing.InputProcessor; import com.example.fitbot.util.processing.InputProcessor;
import org.joml.Vector3f; import org.joml.Vector3f;
@@ -22,14 +24,12 @@ import org.joml.Vector3f;
public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks { public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks {
// Private fields for the FitnessActivity class. // Private fields for the FitnessActivity class.
private PersonalMotionPreviewElement personalMotionPreviewElement; private ExerciseStatusElement personalMotionPreviewElement;
private InputProcessor motionProcessor; private InputProcessor motionProcessor;
private Exercise currentExercise; private Exercise currentExercise;
private QiContext qiContext;
// Some nice little messages for the user // 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.", "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.", "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." ", de oefeningen slapen nog, probeer het later nog eens."
@@ -54,17 +54,16 @@ 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); playVideo(videoView, this);
NavigationManager.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class); NavigationManager.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class);
NavigationManager.hideSystemUI(this); NavigationManager.hideSystemUI(this);
} }
@Override @Override
public void onRobotFocusGained(QiContext qiContext) { public void onRobotFocusGained(QiContext qiContext) {
this.qiContext = qiContext;
// Find the VideoView by its ID // Provide the context so that all queued actions can be performed.
// CompletableFuture.runAsync(() -> FitnessCycle.executeMovement("bicepcurl", 10, qiContext)); Pepper.provideContext(qiContext, this.getClass());
personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement); personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement);
@@ -74,19 +73,18 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
personalMotionPreviewElement.post(() -> { personalMotionPreviewElement.post(() -> {
this.acquireExercise((exercise) -> { this.acquireExercise((exercise) -> {
// Acquire paths from the exercise and provide them to the motion processor // 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); motionProcessor = new InputProcessor(vectors, exercise.exerciseTimeInSeconds, SENSOR_SAMPLE_RATE);
personalMotionPreviewElement.provideQiContext(qiContext);
personalMotionPreviewElement.initialize(exercise, motionProcessor, EXERCISE_COUNT); personalMotionPreviewElement.initialize(exercise, motionProcessor, EXERCISE_COUNT);
motionProcessor.startListening(); motionProcessor.startListening();
motionProcessor.setInputHandler(personalMotionPreviewElement); motionProcessor.setInputHandler(personalMotionPreviewElement);
}, (n) -> { }, (n) -> {
int randomMessageIndex = (int)Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length); 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_MESSAGES[randomMessageIndex]);
Pepper.speak(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE, qiContext); Pepper.speak(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE);
NavigationManager.navigateToActivity(this, MainActivity.class); 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... // therefore we'll have to do it like this...
(new Thread(() -> { (new Thread(() -> {
Exercise exercise = ExerciseManager.retrieveExercise(); Exercise exercise = ExerciseManager.retrieveExercise();
if ( exercise == null) if (exercise == null) {
{
onFailedFetch.handle(null); onFailedFetch.handle(null);
} } else {
else {
onSuccessfulFetch.handle(exercise); onSuccessfulFetch.handle(exercise);
} }
})).start(); })).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 @Override
public void onRobotFocusLost() { public void onRobotFocusLost() {
QiSDK.unregister(this, this); QiSDK.unregister(this, this);
Pepper.provideContext(null, this.getClass()); // Remove the context (unavailable)
// Go to the main screen // Go to the main screen
NavigationManager.navigateToActivity(this, MainActivity.class); NavigationManager.navigateToActivity(this, MainActivity.class);
} }
@Override @Override
public void onRobotFocusRefused(String reason) {} public void onRobotFocusRefused(String reason) {
}
@Override @Override
protected void onDestroy() { protected void onDestroy() {
if ( this.motionProcessor != null ) { if (this.motionProcessor != null) {
this.motionProcessor.stopListening(); this.motionProcessor.stopListening();
this.motionProcessor = null; this.motionProcessor = null;
} }
QiSDK.unregister(this, this); QiSDK.unregister(this, this);
Pepper.provideContext(null, this.getClass());
super.onDestroy(); super.onDestroy();
} }
} }

View File

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

View File

@@ -9,13 +9,12 @@ 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.example.fitbot.exercise.Exercise; 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.EndScreenActivity;
import com.example.fitbot.ui.activities.FitnessActivity; import com.example.fitbot.ui.activities.FitnessActivity;
import com.example.fitbot.ui.activities.MainActivity; import com.example.fitbot.ui.activities.MainActivity;
import com.example.fitbot.util.NavigationManager; 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.IInputHandler;
import com.example.fitbot.util.processing.InputProcessor; import com.example.fitbot.util.processing.InputProcessor;
@@ -26,12 +25,11 @@ import org.joml.Vector4f;
import java.util.concurrent.ConcurrentLinkedQueue; 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. // Fields regarding Exercise and speech handling.
private InputProcessor motionProcessor; private InputProcessor motionProcessor;
private Exercise exercise; private Exercise exercise;
private QiContext qiContext;
private int exerciseCount; private int exerciseCount;
private FitnessActivity parentActivity; private FitnessActivity parentActivity;
@@ -41,10 +39,10 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
private final Paint backgroundPaint = new Paint(); private final Paint backgroundPaint = new Paint();
// TODO: Remove // TODO: Remove
private Matrix4f viewMatrix = new Matrix4f(); // The view matrix for the 3D to 2D transformation. private final 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 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 final ConcurrentLinkedQueue<Vector2f> vectors = new ConcurrentLinkedQueue<>();
// TODO: Remove // TODO: Remove
private Vector2f[] axisVectors = new Vector2f[0]; 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); super(context, attrs);
if ( context instanceof Activity) if (context instanceof Activity) {
{
this.parentActivity = (FitnessActivity) context; this.parentActivity = (FitnessActivity) context;
} }
@@ -96,8 +93,8 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
this.exercise = exercise; this.exercise = exercise;
this.exerciseCount = exerciseCount; this.exerciseCount = exerciseCount;
// TODO: Remove /* TODO: Remove */
this.axisVectors = new Vector2f[] { 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(5.0f, 0, 0), getWidth(), getHeight()), projectVertex(new Vector3f(5.0f, 0, 0), getWidth(), getHeight()),
projectVertex(new Vector3f(0, -5.0f, 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()) 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. // Handler that is called every time the motion processor receives new data.
this.motionProcessor.setInputHandler((rotationVector, deviceId) -> { this.motionProcessor.setInputHandler((rotationVector, deviceId) -> {
Log.i("MotionProcessor", "Rotation vector received: " + rotationVector); Log.i("MotionProcessor", "Rotation vector received: " + rotationVector);
Log.i("MotionProcessor", "Last error offset:" + this.motionProcessor.getError(deviceId, this.motionProcessor.secondsPassed())); 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, // If for some reason the parent activity is not defined,
// move back to the main screen. // move back to the main screen.
if ( this.parentActivity == null) if (this.parentActivity == null) {
{
// Move to main screen // Move to main screen
NavigationManager.navigateToActivity(getContext(), MainActivity.class); NavigationManager.navigateToActivity(getContext(), MainActivity.class);
return; return;
} }
// Move on to the next exercise, or finish. // Move on to the next exercise, or finish.
if ( this.exerciseCount > 0 ) if (this.exerciseCount > 0) {
{
this.exerciseCount--; this.exerciseCount--;
this.parentActivity.acquireExercise((newExercise) -> { this.parentActivity.acquireExercise((newExercise) -> {
this.motionProcessor.useExercise(newExercise); this.motionProcessor.useExercise(newExercise);
@@ -132,43 +128,27 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
// Move to main screen // Move to main screen
NavigationManager.navigateToActivity(parentActivity, MainActivity.class); NavigationManager.navigateToActivity(parentActivity, MainActivity.class);
}); });
} } else {
else
{
// Finish the exercise. // Finish the exercise.
NavigationManager.navigateToActivity(parentActivity, EndScreenActivity.class); NavigationManager.navigateToActivity(parentActivity, EndScreenActivity.class);
return; return;
} }
} }
// TODO: Adjust / remove /* TODO: Use raw vector */
vectors.add(projectVertex(rotationVector, this.getWidth(), this.getHeight())); 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()); 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 // 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();
}); });
} }
/**
* 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. * 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 @Override
public void onDraw(Canvas canvas) { public void onDraw(Canvas canvas) {
canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint); canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint);
@@ -211,17 +190,17 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
/*if (this.exercise == null) /*if (this.exercise == null)
return;*/ return;*/
for (int i = 0, startX, endX, startY, endY; i < axisVectors.length/2; i++)
{ /* TODO: Remove */
startX = (int)Math.max(0, Math.min(this.getWidth(), (int)axisVectors[i*2].x)); for (int i = 0, startX, endX, startY, endY; i < axisVectors.length / 2; i++) {
endX = (int)Math.max(0, Math.min(this.getWidth(), (int)axisVectors[i*2+1].x)); startX = (int) Math.max(0, Math.min(this.getWidth(), (int) axisVectors[i * 2].x));
startY = (int)Math.max(0, Math.min(this.getHeight(), (int)axisVectors[i*2].y)); endX = (int) Math.max(0, Math.min(this.getWidth(), (int) axisVectors[i * 2 + 1].x));
endY = (int)Math.max(0, Math.min(this.getHeight(), (int)axisVectors[i*2+1].y)); 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); 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); 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 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()); int resId = ctx.getResources().getIdentifier(AnimationFile, "raw", ctx.getPackageName());
Animation animation = AnimationBuilder.with(ctx) 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,10 +6,6 @@ import android.content.Intent;
import android.view.View; import android.view.View;
import android.widget.Button; 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 { public class NavigationManager {
/** /**
@@ -21,8 +17,7 @@ public class NavigationManager {
*/ */
public static void setupButtonNavigation(Activity currentActivity, int buttonId, Class<? extends Activity> targetActivity) { public static void setupButtonNavigation(Activity currentActivity, int buttonId, Class<? extends Activity> targetActivity) {
Button button = currentActivity.findViewById(buttonId); Button button = currentActivity.findViewById(buttonId);
if ( button == null ) if (button == null) {
{
throw new IllegalArgumentException("Button with ID " + buttonId + " not found in " + currentActivity.getClass().getSimpleName()); throw new IllegalArgumentException("Button with ID " + buttonId + " not found in " + currentActivity.getClass().getSimpleName());
} }
button.setOnClickListener(v -> NavigationManager.navigateToActivity(currentActivity, targetActivity)); button.setOnClickListener(v -> NavigationManager.navigateToActivity(currentActivity, targetActivity));

View File

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

View File

@@ -84,7 +84,7 @@ public class PathSegment {
* @return The distance to the closest point on the path segment. * @return The distance to the closest point on the path segment.
*/ */
public double distance(Vector3f reference) { 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.end.distance(reference);
return this.start.distance(reference); return this.start.distance(reference);
} }

View File

@@ -6,6 +6,7 @@ public interface IInputHandler {
/** /**
* Function for accepting motion data and the transformed vector. * Function for accepting motion data and the transformed vector.
*
* @param rotationVector The rotation vector of the motion data. * @param rotationVector The rotation vector of the motion data.
* @param sensorId The sensor ID of the motion data. * @param sensorId The sensor ID of the motion data.
*/ */

View File

@@ -49,6 +49,7 @@ public class InputProcessor {
* Function for setting the exercise to use. * Function for setting the exercise to use.
* This updates the user and target path and the * This updates the user and target path and the
* duration of the exercise. * duration of the exercise.
*
* @param exercise The exercise to use the paths for. * @param exercise The exercise to use the paths for.
*/ */
public void useExercise(Exercise exercise) { public void useExercise(Exercise exercise) {
@@ -61,6 +62,7 @@ public class InputProcessor {
/** /**
* Function for checking if the exercise has finished. * Function for checking if the exercise has finished.
*
* @return True if the exercise has finished, false otherwise. * @return True if the exercise has finished, false otherwise.
*/ */
public boolean hasFinished() { public boolean hasFinished() {
@@ -171,8 +173,7 @@ public class InputProcessor {
* *
* @return The current progress of the exercise. * @return The current progress of the exercise.
*/ */
public double getCurrentProgress() public double getCurrentProgress() {
{
return secondsPassed / exerciseDuration; return secondsPassed / exerciseDuration;
} }
@@ -208,8 +209,7 @@ public class InputProcessor {
// Ensure the indexes are within the bounds of the array // Ensure the indexes are within the bounds of the array
if (targetIndex >= 0 && targetIndex <= this.targetRotationVectorPaths[sensorId].length - 1 && 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 this.selfRotationVectorPaths[sensorId][selfIndex].distance(this.targetRotationVectorPaths[sensorId][targetIndex]);
} }
return 0.0d; return 0.0d;

View File

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

View File

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

View File

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