Updated exercise handling code to make pepper more verbal

This commit is contained in:
Luca Warmenhoven
2024-05-30 15:38:11 +02:00
parent c6875d2ac7
commit 5c3b84b3ae
9 changed files with 153 additions and 96 deletions

View File

@@ -40,4 +40,8 @@ public class Exercise {
this.videoUrl = videoUrl; this.videoUrl = videoUrl;
this.exerciseTimeInSeconds = exerciseTimeInSeconds; this.exerciseTimeInSeconds = exerciseTimeInSeconds;
} }
public interface ExerciseFetchHandler {
void handle(Exercise exercise);
}
} }

View File

@@ -18,23 +18,28 @@ public class ExerciseManager {
// The value of these property variables must be equivalent of // The value of these property variables must be equivalent of
// the JSON data that the database sends back. // the JSON data that the database sends back.
// If this is not the case then the exercise retrieval will fail. // If this is not the case then the exercise retrieval will fail.
private static final String PROPERTY_MUSCLE_GROUP = "muscleGroup"; private static final String PROPERTY_EXERCISE_DURATION = "duration";
private static final String PROPERTY_DESC = "description"; private static final String PROPERTY_EXERCISE_ID = "exerciseId";
private static final String PROPERTY_IMAGE_URL = "imageUrl"; private static final String PROPERTY_MUSCLE_GROUP = "muscleGroup";
private static final String PROPERTY_VIDEO_URL = "videoUrl"; private static final String PROPERTY_SHORT_DESC = "shortDescription";
private static final String PROPERTY_NAME = "name"; private static final String PROPERTY_IMAGE_URL = "imageUrl";
private static final String PROPERTY_DATA = "data"; private static final String PROPERTY_VIDEO_URL = "videoUrl";
private static final String PROPERTY_EXERCISE_DURATION = "exerciseDuration"; 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; public static final int SENSOR_COUNT = 2;
private static final String[] REQUIRED_PROPERTIES = { private static final String[] REQUIRED_PROPERTIES = {
PROPERTY_MUSCLE_GROUP, PROPERTY_DESC, PROPERTY_IMAGE_URL, PROPERTY_MUSCLE_GROUP, PROPERTY_DESC, PROPERTY_IMAGE_URL,
PROPERTY_VIDEO_URL, PROPERTY_NAME, PROPERTY_DATA, PROPERTY_VIDEO_URL, PROPERTY_NAME, PROPERTY_PATH,
PROPERTY_EXERCISE_DURATION PROPERTY_EXERCISE_DURATION, PROPERTY_EXERCISE_ID,
PROPERTY_SHORT_DESC
}; };
private static final float DEFAULT_SEGMENT_SPEED = 1.0f; 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. * Function for sending an HTTP request to the server.
@@ -95,7 +100,7 @@ public class ExerciseManager {
// Path data is split into two parts, due to the left and right hand. // Path data is split into two parts, due to the left and right hand.
// If one wants to add support for more sensors, one will have to adjust the Exercise // If one wants to add support for more sensors, one will have to adjust the Exercise
// class to support more paths. // class to support more paths.
String[] leftRightData = content.get(PROPERTY_DATA).getAsString().split(";"); String[] leftRightData = content.get(PROPERTY_PATH).getAsString().split(";");
if ( leftRightData.length != SENSOR_COUNT) if ( leftRightData.length != SENSOR_COUNT)
return null; return null;

View File

@@ -4,8 +4,7 @@ import android.os.Bundle;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import com.example.fitbot.R; import com.example.fitbot.R;
import com.example.fitbot.ui.activities.FitnessActivity; import com.example.fitbot.util.NavigationManager;
import com.example.fitbot.ui.activities.MainActivity;
public class EndScreenActivity extends AppCompatActivity { public class EndScreenActivity extends AppCompatActivity {
@@ -14,7 +13,7 @@ public class EndScreenActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_end_screen); setContentView(R.layout.activity_end_screen);
com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonEndScreen, MainActivity.class); NavigationManager.setupButtonNavigation(this, R.id.homeButtonEndScreen, MainActivity.class);
com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.startButtonEndScreen, FitnessActivity.class); NavigationManager.setupButtonNavigation(this, R.id.startButtonEndScreen, FitnessActivity.class);
} }
} }

View File

@@ -1,8 +1,6 @@
package com.example.fitbot.ui.activities; package com.example.fitbot.ui.activities;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.VideoView; import android.widget.VideoView;
import com.aldebaran.qi.sdk.QiContext; import com.aldebaran.qi.sdk.QiContext;
@@ -14,7 +12,7 @@ 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.ui.components.PersonalMotionPreviewElement; import com.example.fitbot.ui.components.PersonalMotionPreviewElement;
import com.example.fitbot.util.ButtonNavigation; import com.example.fitbot.util.NavigationManager;
import com.example.fitbot.util.FitnessCycle; import com.example.fitbot.util.FitnessCycle;
import com.example.fitbot.util.processing.InputProcessor; import com.example.fitbot.util.processing.InputProcessor;
@@ -56,7 +54,7 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
// Find the VideoView by its ID // Find the VideoView by its ID
VideoView videoView = findViewById(R.id.videoView); VideoView videoView = findViewById(R.id.videoView);
FitnessCycle.playVideo(videoView, this); FitnessCycle.playVideo(videoView, this);
ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class); NavigationManager.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class);
// Implement your logic when the robot focus is gained // Implement your logic when the robot focus is gained
} }
@@ -73,22 +71,24 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
// This will provide the element with the appropriate dimensions for drawing // This will provide the element with the appropriate dimensions for drawing
// the canvas properly. // the canvas properly.
personalMotionPreviewElement.post(() -> { personalMotionPreviewElement.post(() -> {
Exercise exercise = this.acquireExercise(); this.acquireExercise((exercise) -> {
if ( exercise == null) { // Acquire paths from the exercise and provide them to the motion processor
return; Vector3f[][] vectors = new Vector3f[][] {exercise.leftPath.getVectors(), exercise.rightPath.getVectors()};
}
// Acquire paths from the exercise and provide them to the motion processor
Vector3f[][] vectors = new Vector3f[][] {exercise.leftPath.getVectors(), exercise.rightPath.getVectors()};
motionProcessor = new InputProcessor(vectors, exercise.exerciseTimeInSeconds, SENSOR_SAMPLE_RATE); motionProcessor = new InputProcessor(vectors, exercise.exerciseTimeInSeconds, SENSOR_SAMPLE_RATE);
personalMotionPreviewElement.provideQiContext(qiContext); 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) -> {
int randomMessageIndex = (int)Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length);
FitnessCycle.say(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex], qiContext);
FitnessCycle.say(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE, qiContext);
});
}); });
personalMotionPreviewElement.provideQiContext(qiContext);
// FitnessCycle.playVideo(qiContext, videoView, this); // FitnessCycle.playVideo(qiContext, videoView, this);
} }
@@ -97,28 +97,32 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
* Acquire an exercise from the ExerciseManager. * Acquire an exercise from the ExerciseManager.
* Whenever the retrieval failed, it will have the robot say something to the user * Whenever the retrieval failed, it will have the robot say something to the user
* to inform them about the issue. * to inform them about the issue.
*
* @return The acquired exercise, or null if the exercise could not be retrieved.
*/ */
public Exercise acquireExercise() { public void acquireExercise(Exercise.ExerciseFetchHandler onSuccessfulFetch, Exercise.ExerciseFetchHandler onFailedFetch) {
Exercise exercise = ExerciseManager.retrieveExercise(); // For some stupid reason we cannot perform network operations on the main thread.
if ( exercise == null && this.qiContext != null) // therefore we'll have to do it like this...
{ (new Thread(() -> {
int randomMessageIndex = (int)Math.floor(Math.random() * EXERCISE_NOT_FOUND_MESSAGES.length); Exercise exercise = ExerciseManager.retrieveExercise();
FitnessCycle.say(EXERCISE_NOT_FOUND_MESSAGES[randomMessageIndex], qiContext); if ( exercise == null)
FitnessCycle.say(EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE, qiContext); {
} onFailedFetch.handle(null);
return exercise; }
else {
onSuccessfulFetch.handle(exercise);
}
})).start();
} }
@Override @Override
public void onRobotFocusLost() { public void onRobotFocusLost() {
// Implement your logic when the robot focus is lost QiSDK.unregister(this, this);
// Go to the main screen
NavigationManager.navigateToActivity(this, MainActivity.class);
} }
@Override @Override
public void onRobotFocusRefused(String reason) { public void onRobotFocusRefused(String reason) {
// Implement your logic when the robot focus is refused QiSDK.unregister(this, this);
} }
@Override @Override
@@ -127,6 +131,5 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall
QiSDK.unregister(this, this); QiSDK.unregister(this, this);
this.motionProcessor.stopListening(); this.motionProcessor.stopListening();
this.motionProcessor = null; this.motionProcessor = null;
this.personalMotionPreviewElement.destroy();
} }
} }

View File

@@ -5,7 +5,7 @@ import android.os.Bundle;
import android.view.View; import android.view.View;
import com.example.fitbot.R; import com.example.fitbot.R;
import com.example.fitbot.util.ButtonNavigation; import com.example.fitbot.util.NavigationManager;
public class HelpActivity extends AppCompatActivity { public class HelpActivity extends AppCompatActivity {
@@ -14,7 +14,7 @@ public class HelpActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_help); setContentView(R.layout.activity_help);
ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonHelp, MainActivity.class); NavigationManager.setupButtonNavigation(this, R.id.homeButtonHelp, MainActivity.class);
// Hide system UI // Hide system UI
hideSystemUI(); hideSystemUI();

View File

@@ -10,13 +10,12 @@ import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import com.example.fitbot.R; import com.example.fitbot.R;
import com.example.fitbot.util.ButtonNavigation; import com.example.fitbot.util.NavigationManager;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
@@ -63,8 +62,8 @@ public class MainActivity extends AppCompatActivity {
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().hide(); getSupportActionBar().hide();
} }
ButtonNavigation.setupButtonNavigation(this, R.id.startButtonMain, FitnessActivity.class); NavigationManager.setupButtonNavigation(this, R.id.startButtonMain, FitnessActivity.class);
ButtonNavigation.setupButtonNavigation(this, R.id.helpButtonMain, HelpActivity.class); NavigationManager.setupButtonNavigation(this, R.id.helpButtonMain, HelpActivity.class);
/*---Tool Bar---*/ /*---Tool Bar---*/
setSupportActionBar(toolbar); // Make the toolbar act as the action bar setSupportActionBar(toolbar); // Make the toolbar act as the action bar

View File

@@ -2,7 +2,6 @@ package com.example.fitbot.ui.components;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@@ -15,6 +14,7 @@ import com.example.fitbot.exercise.Exercise;
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.FitnessCycle; 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;
@@ -105,25 +105,6 @@ 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()),
projectVertex(new Vector3f(0, 0, 5.0f), getWidth(), getHeight()) projectVertex(new Vector3f(0, 0, 5.0f), getWidth(), getHeight())
}; };
}
public void destroy()
{
if ( this.motionProcessor != null )
this.motionProcessor.stopListening();
this.motionProcessor = null;
}
/**
* 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;
// 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) -> {
@@ -133,27 +114,29 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
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 // Move to main screen
this.destroy(); NavigationManager.navigateToActivity(getContext(), MainActivity.class);
Intent intent = new Intent(getContext(), MainActivity.class);
getContext().startActivity(intent);
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.exercise = this.parentActivity.acquireExercise(); this.parentActivity.acquireExercise((newExercise) -> {
this.motionProcessor.useExercise(this.exercise); this.motionProcessor.useExercise(newExercise);
}, (failed) -> {
// Move to main screen
NavigationManager.navigateToActivity(getContext(), MainActivity.class);
});
} }
else else
{ {
// Finish the exercise. // Finish the exercise.
this.destroy(); NavigationManager.navigateToActivity(getContext(), EndScreenActivity.class);
Intent intent = new Intent(getContext(), EndScreenActivity.class);
getContext().startActivity(intent);
return; return;
} }
} }
@@ -168,6 +151,17 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
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) if (this.qiContext == null)
return; return;

View File

@@ -7,6 +7,8 @@ import android.util.Log;
import android.widget.VideoView; import android.widget.VideoView;
import com.aldebaran.qi.sdk.builder.SayBuilder; 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.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;
@@ -17,19 +19,25 @@ import com.aldebaran.qi.sdk.builder.AnimationBuilder;
import com.aldebaran.qi.sdk.object.actuation.Animate; import com.aldebaran.qi.sdk.object.actuation.Animate;
import com.aldebaran.qi.sdk.object.actuation.Animation; 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; import java.util.concurrent.atomic.AtomicInteger;
public class FitnessCycle extends AppCompatActivity { public class FitnessCycle extends AppCompatActivity {
private static final Locale DUTCH_LOCALE = new Locale(Language.DUTCH, Region.NETHERLANDS); 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 * Function for executing a movement animation a certain number of times
* on the robot * on the robot
* *
* @param Exercise The name of the exercise to perform * @param Exercise The name of the exercise to perform
* @param Reps The number of repetitions to perform * @param Reps The number of repetitions to perform
* @param qiContext The QiContext to use * @param qiContext The QiContext to use
*/ */
public static void executeMovement(String Exercise, int Reps, QiContext qiContext) { public static void executeMovement(String Exercise, int Reps, QiContext qiContext) {
@@ -59,35 +67,46 @@ public class FitnessCycle extends AppCompatActivity {
/** /**
* Function for making the robot say something with DUTCH_LOCALE as locale * Function for making the robot say something with DUTCH_LOCALE as locale
*
* @param phrase The phrase to make the robot say * @param phrase The phrase to make the robot say
* @param ctx The QiContext to use * @param ctx The QiContext to use
*/ */
public static void say(String phrase, QiContext ctx) public static void say(String phrase, QiContext ctx) {
{
say(phrase, ctx, DUTCH_LOCALE); say(phrase, ctx, DUTCH_LOCALE);
} }
/** /**
* Function for making the robot say something with a specific locale * Function for making the robot say something with a specific locale
*
* @param phrase The phrase to make the robot say * @param phrase The phrase to make the robot say
* @param ctx The QiContext to use * @param ctx The QiContext to use
* @param locale The locale to use * @param locale The locale to use
*/ */
public static void say(String phrase, QiContext ctx, Locale locale) public static void say(String phrase, QiContext ctx, Locale locale) {
{ // If the robot is already speaking, add the phrase to the queue
SayBuilder MESSAGE_QUEUE.add(new PepperSpeechEntry(phrase, ctx, locale));
.with(ctx) processMessageQueue();
.withLocale(locale) }
.withText(phrase)
.build() private static synchronized void processMessageQueue() {
.run(); 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 * Function for playing a video in a VideoView
* *
* @param videoView The VideoView to play the video in * @param videoView The VideoView to play the video in
* @param context The context to use * @param context The context to use
*/ */
public static void playVideo(VideoView videoView, Context context) { public static void playVideo(VideoView videoView, Context context) {
// Set up the video player // Set up the video player
@@ -105,4 +124,24 @@ public class FitnessCycle extends AppCompatActivity {
Log.e("FitnessActivity", "VideoView is null. Check your layout XML."); Log.e("FitnessActivity", "VideoView is null. Check your layout XML.");
} }
} }
private static class PepperSpeechEntry {
public String phrase;
public QiContext context;
public Locale locale;
public PepperSpeechEntry(String phrase, QiContext context, Locale locale) {
this.phrase = phrase;
this.context = context;
this.locale = locale;
}
public Say getSay() {
return SayBuilder
.with(context)
.withLocale(locale)
.withText(phrase)
.build();
}
}
} }

View File

@@ -6,7 +6,10 @@ import android.content.Intent;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
public class ButtonNavigation { import com.example.fitbot.ui.activities.FitnessActivity;
import com.example.fitbot.ui.activities.MainActivity;
public class NavigationManager {
/** /**
* Sets up a button to navigate to a different activity when clicked. * Sets up a button to navigate to a different activity when clicked.
@@ -23,4 +26,15 @@ public class ButtonNavigation {
currentActivity.finish(); currentActivity.finish();
}); });
} }
/**
* Navigates to the target activity.
*
* @param currentContext The current context
* @param targetActivity The target activity
*/
public static void navigateToActivity(Context currentContext, Class<? extends Activity> targetActivity) {
Intent intent = new Intent(currentContext, targetActivity);
currentContext.startActivity(intent);
}
} }