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.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 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_DESC = "description";
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_DATA = "data";
private static final String PROPERTY_EXERCISE_DURATION = "exerciseDuration";
public static final int SENSOR_COUNT = 2;
private static final String[] REQUIRED_PROPERTIES = {
PROPERTY_MUSCLE_GROUP, PROPERTY_DESC, PROPERTY_IMAGE_URL,
PROPERTY_VIDEO_URL, PROPERTY_NAME, PROPERTY_DATA,
PROPERTY_EXERCISE_DURATION
PROPERTY_VIDEO_URL, PROPERTY_NAME, PROPERTY_PATH,
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.
@@ -95,7 +100,7 @@ public class ExerciseManager {
// Path data is split into two parts, due to the left and right hand.
// If one wants to add support for more sensors, one will have to adjust the Exercise
// class to support more paths.
String[] leftRightData = content.get(PROPERTY_DATA).getAsString().split(";");
String[] leftRightData = content.get(PROPERTY_PATH).getAsString().split(";");
if ( leftRightData.length != SENSOR_COUNT)
return null;

View File

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

View File

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

View File

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

View File

@@ -10,13 +10,12 @@ import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import com.example.fitbot.R;
import com.example.fitbot.util.ButtonNavigation;
import com.example.fitbot.util.NavigationManager;
public class MainActivity extends AppCompatActivity {
@@ -63,8 +62,8 @@ public class MainActivity extends AppCompatActivity {
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
ButtonNavigation.setupButtonNavigation(this, R.id.startButtonMain, FitnessActivity.class);
ButtonNavigation.setupButtonNavigation(this, R.id.helpButtonMain, HelpActivity.class);
NavigationManager.setupButtonNavigation(this, R.id.startButtonMain, FitnessActivity.class);
NavigationManager.setupButtonNavigation(this, R.id.helpButtonMain, HelpActivity.class);
/*---Tool 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.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Paint;
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.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;
@@ -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())
};
}
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.
this.motionProcessor.setInputHandler((rotationVector, deviceId) -> {
@@ -133,27 +114,29 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
if ( this.motionProcessor.hasFinished())
{
// If for some reason the parent activity is not defined,
// move back to the main screen.
if ( this.parentActivity == null)
{
// Move to main screen
this.destroy();
Intent intent = new Intent(getContext(), MainActivity.class);
getContext().startActivity(intent);
NavigationManager.navigateToActivity(getContext(), MainActivity.class);
return;
}
// Move on to the next exercise, or finish.
if ( this.exerciseCount > 0 )
{
this.exerciseCount--;
this.exercise = this.parentActivity.acquireExercise();
this.motionProcessor.useExercise(this.exercise);
this.parentActivity.acquireExercise((newExercise) -> {
this.motionProcessor.useExercise(newExercise);
}, (failed) -> {
// Move to main screen
NavigationManager.navigateToActivity(getContext(), MainActivity.class);
});
}
else
{
// Finish the exercise.
this.destroy();
Intent intent = new Intent(getContext(), EndScreenActivity.class);
getContext().startActivity(intent);
NavigationManager.navigateToActivity(getContext(), EndScreenActivity.class);
return;
}
}
@@ -168,6 +151,17 @@ public class PersonalMotionPreviewElement extends View implements IInputHandler
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;

View File

@@ -7,6 +7,8 @@ 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;
@@ -17,12 +19,18 @@ 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
@@ -59,28 +67,39 @@ public class FitnessCycle extends AppCompatActivity {
/**
* 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)
{
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)
{
SayBuilder
.with(ctx)
.withLocale(locale)
.withText(phrase)
.build()
.run();
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
});
}
}
/**
@@ -105,4 +124,24 @@ public class FitnessCycle extends AppCompatActivity {
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.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.
@@ -23,4 +26,15 @@ public class ButtonNavigation {
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);
}
}