diff --git a/code/src/Fitbot/.idea/misc.xml b/code/src/Fitbot/.idea/misc.xml index 4496bac..a647bf6 100644 --- a/code/src/Fitbot/.idea/misc.xml +++ b/code/src/Fitbot/.idea/misc.xml @@ -16,25 +16,33 @@ - + + + + + - + + + + - - + + + diff --git a/code/src/Fitbot/app/build.gradle b/code/src/Fitbot/app/build.gradle index e63614b..1348df8 100644 --- a/code/src/Fitbot/app/build.gradle +++ b/code/src/Fitbot/app/build.gradle @@ -38,9 +38,6 @@ dependencies { implementation 'org.joml:joml:1.10.5' implementation 'com.google.code.gson:gson:2.8.6' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.junit.jupiter:junit-jupiter' - testImplementation 'org.junit.jupiter:junit-jupiter' - testImplementation 'org.junit.jupiter:junit-jupiter' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation 'com.aldebaran:qisdk:1.7.5' diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/EMuscleGroup.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/EMuscleGroup.java index 27ff244..e85d0da 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/EMuscleGroup.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/EMuscleGroup.java @@ -3,21 +3,31 @@ package com.example.fitbot.exercise; public enum EMuscleGroup { // TODO: Implement - TORSO(0), - ARMS(1), - LEGS(2), - BALANCE(3); + TORSO(0, new String[]{"upper body", "torso"}), + ARMS(1, new String[]{"arms", "arm", "shoulder"}), + LEGS(2, new String[]{"Lower body", "legs", "leg"}); int muscleGroupIdentifier; + String[] muscleGroupNames; - EMuscleGroup(int identifier) { + EMuscleGroup(int identifier, String[] muscleGroupNames) { this.muscleGroupIdentifier = identifier; + this.muscleGroupNames = muscleGroupNames; } public int getIdentifier() { return this.muscleGroupIdentifier; } + public static EMuscleGroup parse(String name) + { + for ( EMuscleGroup muscleGroup : EMuscleGroup.values()) + for ( String muscleGroupName : muscleGroup.muscleGroupNames) + if ( muscleGroupName.equalsIgnoreCase(name)) + return muscleGroup; + return null; + } + public static EMuscleGroup parse(int identifier) { for (EMuscleGroup muscleGroup : EMuscleGroup.values()) { if (muscleGroup.getIdentifier() == identifier) { diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java index 20758b5..d8198b6 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/Exercise.java @@ -3,26 +3,21 @@ package com.example.fitbot.exercise; import android.util.Log; import com.example.fitbot.util.path.GesturePath; -import com.example.fitbot.util.processing.IMotionDataConsumer; import com.example.fitbot.util.server.IWebServerHandler; import com.example.fitbot.util.server.WebServer; import java.util.Objects; -import java.util.function.Consumer; -public class Exercise implements IWebServerHandler { - - private EMuscleGroup muscleGroup; - private GesturePath leftPath; - private GesturePath rightPath; - private String title; - private String description; - private float segmentsPerSecond; - - // Static fields. - private static WebServer webSocket; - private static Exercise currentExercise = null; +public class Exercise { + public final EMuscleGroup muscleGroup; + public final GesturePath leftPath; + public final GesturePath rightPath; + public final String title; + public final String description; + public final String imageUrl; + public final String videoUrl; + public final float exerciseTimeInSeconds; /** * Constructor for the AbstractExercise class. @@ -32,106 +27,17 @@ public class Exercise implements IWebServerHandler { * @param rightPath The path of the right hand. * @param title The title of the exercise. * @param description The description of the exercise. - * @param segmentsPerSecond The number of segments per second. - * This determines how fast the exercise should be performed. + * @param imageUrl The URL of the image. + * @param videoUrl The URL of the video. */ - public Exercise(EMuscleGroup muscleGroup, String title, String description, GesturePath leftPath, GesturePath rightPath) { + public Exercise(EMuscleGroup muscleGroup, String title, String description, String imageUrl, String videoUrl, GesturePath leftPath, GesturePath rightPath, float exerciseTimeInSeconds) { this.muscleGroup = muscleGroup; this.title = title; this.description = description; this.leftPath = leftPath; this.rightPath = rightPath; - } - - /** - * Start the exercise. - * This method starts a WebSocket server - */ - public final void startExercise() { - - // Ensure no other exercise is active. - if (currentExercise != null && currentExercise != this) { - currentExercise.__stopExercise(); - Log.i("Exercises", "Another exercise was started when another was still running."); - } - - // If a WebSocket server is already running, change the event handler to be this class. - if (webSocket != null && webSocket.isConnected()) { - webSocket.setEventHandler(this); - } - - try { - webSocket = WebServer.createServer(); - Objects.requireNonNull(webSocket, "WebSocket server could not be created."); - - webSocket.setEventHandler(this); - currentExercise = this; - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Method for ending this exercise and returning the grade of the performance - * of this activity. - */ - public final double finishExercise() { - this.__stopExercise(); - - // TODO: Implement grade calculation - return 0.0; - } - - /** - * Stop the exercise. - * This method stops the WebSocket server. - */ - private void __stopExercise() { - if (webSocket != null && webSocket.isConnected()) { - webSocket.stop(); - webSocket = null; - } - currentExercise = null; - } - - /** - * Check if the current exercise is the current activity. - */ - public final boolean isCurrentActivity() { - return currentExercise == this; - } - - /** - * Get the muscle group of the exercise. - */ - public EMuscleGroup getMuscleGroup() { - return muscleGroup; - } - - /** - * Get the path of the exercise. - */ - public GesturePath[] getPath() { - return new GesturePath[]{leftPath, rightPath}; - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - /** - * Get the speed of the exercise. - */ - public double getSegmentsPerSecond() { - return segmentsPerSecond; - } - - @Override - public void onReceive(String message) { - + this.imageUrl = imageUrl; + this.videoUrl = videoUrl; + this.exerciseTimeInSeconds = exerciseTimeInSeconds; } } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/ExerciseManager.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/ExerciseManager.java index a500943..e4ad128 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/ExerciseManager.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/exercise/ExerciseManager.java @@ -4,25 +4,35 @@ import com.example.fitbot.util.path.GesturePath; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import org.joml.Vector3f; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.lang.reflect.Constructor; import java.net.URL; import java.net.URLConnection; public class ExerciseManager { - private static final String HOST_ADDRESS = "http://145.92.8.132"; + private static final String HOST_ADDRESS = "http://145.92.8.132:443/"; + // 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_MUSCLE_GROUP = "muscleGroup"; private static final String PROPERTY_DESC = "description"; - private static final String PROPERTY_VECTORS = "vector_data"; + private static final String PROPERTY_IMAGE_URL = "imageUrl"; + private static final String PROPERTY_VIDEO_URL = "videoUrl"; private static final String PROPERTY_NAME = "name"; - private static final String PROPERTY_MUSCLE_GROUP = "muscle_group"; - private static final String PROPERTY_SEGMENT_SPEED = "segment_speed"; + 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 + }; private static final float DEFAULT_SEGMENT_SPEED = 1.0f; @@ -36,13 +46,17 @@ public class ExerciseManager { * * @return The response from the server. */ - private static String sendHTTP(String url, String method, String contentType, String body) { + public static String sendHTTP(String url, String method, String contentType, String body) { try { URLConnection connection = new URL(url).openConnection(); + connection.setDoOutput(true); + connection.setDoInput(true); connection.addRequestProperty("Content-Type", contentType); connection.addRequestProperty("Request-Method", method); - connection.getOutputStream().write(body.getBytes()); connection.connect(); + // Send a body if it is present + if (body != null) + connection.getOutputStream().write(body.getBytes()); InputStream stream = connection.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); StringBuilder builder = new StringBuilder(); @@ -60,23 +74,41 @@ public class ExerciseManager { /** * Function for retrieving an exercise from the Raspberry Pi Database. * - * @param uniqueIdentifier The unique identifier of the exercise * @return The exercise, if it exists on the server. Otherwise null. */ - public static Exercise retrieveExercise(String uniqueIdentifier) { + public static Exercise retrieveExercise() { String response = sendHTTP( - HOST_ADDRESS + "/acquire", "GET", "application/json", "{\"kind\":\"" + uniqueIdentifier + "\"}" + HOST_ADDRESS, "POST", "application/json", null ); // Validate the response if (response != null) { try { JsonObject content = JsonParser.parseString(response).getAsJsonObject(); + + // Ensure all required properties are present + for (String property : REQUIRED_PROPERTIES) { + if (!content.has(property)) { + return null; + } + } + + // 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(";"); + + if ( leftRightData.length != SENSOR_COUNT) + return null; + return new Exercise( - EMuscleGroup.parse(content.get(PROPERTY_MUSCLE_GROUP).getAsInt()), + EMuscleGroup.parse(content.get(PROPERTY_MUSCLE_GROUP).getAsString()), content.get(PROPERTY_NAME).getAsString(), content.get(PROPERTY_DESC).getAsString(), - gesturePathFromString(content.get(PROPERTY_VECTORS).getAsString()), - gesturePathFromString(content.get(PROPERTY_SEGMENT_SPEED).getAsString()) + content.get(PROPERTY_IMAGE_URL).getAsString(), + content.get(PROPERTY_VIDEO_URL).getAsString(), + GesturePath.fromString(leftRightData[0]), + GesturePath.fromString(leftRightData[1]), + DEFAULT_SEGMENT_SPEED ); } catch (Exception e) { e.printStackTrace(); @@ -84,42 +116,4 @@ public class ExerciseManager { } return null; } - - /** - * 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. - * - * Note: ASCII to Vector conversion is done in Big Endian format (most significant byte first). - * - * @param input The string to convert - * @return The GesturePath object - */ - private static GesturePath gesturePathFromString(String input) { - byte[] bytes = input.getBytes(); - - // Check if the input string contains a valid amount of bytes (12 bytes per vector) - if (input.length() % 12 != 0) { - throw new IllegalArgumentException("Invalid input string length"); - } - GesturePath.Builder builder = new GesturePath.Builder(); - - float[] xyz = new float[3]; - for (int i = 0; i < bytes.length; i += 12) { - for (int j = 0; j < 3; j++) { - - xyz[j] = Float.intBitsToFloat( - (bytes[i + j * 4] & 0xFF) << 24 | - (bytes[i + j * 4 + 1] & 0xFF) << 16 | - (bytes[i + j * 4 + 2] & 0xFF) << 8 | - (bytes[i + j * 4 + 3] & 0xFF) - ); - } - builder.addVector(new Vector3f( - xyz[0], xyz[1], xyz[2] - )); - } - return builder.build(); - } - } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java index 157bebf..703c53b 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/EndScreenActivity.java @@ -14,7 +14,7 @@ public class EndScreenActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_end_screen); - com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.homeButton, MainActivity.class); - com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.continueButton, FitnessActivity.class); + com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonEndScreen, MainActivity.class); + com.example.fitbot.util.ButtonNavigation.setupButtonNavigation(this, R.id.startButtonEndScreen, FitnessActivity.class); } } \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java index b311917..5d2d6dd 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/FitnessActivity.java @@ -2,6 +2,7 @@ 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; @@ -10,25 +11,43 @@ import com.aldebaran.qi.sdk.RobotLifecycleCallbacks; import com.aldebaran.qi.sdk.design.activity.RobotActivity; import com.aldebaran.qi.sdk.design.activity.conversationstatus.SpeechBarDisplayStrategy; import com.example.fitbot.R; -import com.example.fitbot.exercise.EMuscleGroup; 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.FitnessCycle; -import com.example.fitbot.util.path.GesturePath; +import com.example.fitbot.util.processing.InputProcessor; import org.joml.Vector3f; -import java.util.concurrent.CompletableFuture; - public class FitnessActivity extends RobotActivity implements RobotLifecycleCallbacks { - PersonalMotionPreviewElement personalMotionPreviewElement; + // Private fields for the FitnessActivity class. + private PersonalMotionPreviewElement 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[] { + "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.", + "Ssst, de oefeningen slapen nog, probeer het later nog eens." + }; + + private static final String EXERCISE_NOT_FOUND_SEEK_HELP_MESSAGE = + "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 float EXERCISE_SPEED_MULTIPLIER = 1.0f; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); QiSDK.register(this, this); + setContentView(R.layout.activity_fitness); // Remove the ugly ass bar on top of the view @@ -36,50 +55,62 @@ 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.homeButton, MainActivity.class); - ButtonNavigation.setupButtonNavigation(this, R.id.buttonComplete, EndScreenActivity.class); + ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonFitness, MainActivity.class); // Implement your logic when the robot focus is gained - GesturePath.Builder gesturePathBuilder = new GesturePath.Builder(); - - /* Generate a random path to test the tracking system */ - for ( int i = 0; i < 40; i++) - { - gesturePathBuilder.addVector( - new Vector3f( - (float)Math.cos(Math.PI + (Math.PI / 40.0f) * i), - (float)Math.sin(Math.PI + (Math.PI / 40.0f) * i), - 0 - ) - ); - } - - personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement); - personalMotionPreviewElement.post(() -> { - Log.i("FitnessActivity", "PersonalMotionPreviewElement.post()"); - - Exercise exercise = new Exercise(EMuscleGroup.ARMS, "Bicep Curls", "Oefening voor de biceps.", gesturePathBuilder.build(), gesturePathBuilder.build()); - - personalMotionPreviewElement.initialize(exercise); - personalMotionPreviewElement.provideQiContext(null); - }); - } @Override public void onRobotFocusGained(QiContext qiContext) { - + this.qiContext = qiContext; // Find the VideoView by its ID // CompletableFuture.runAsync(() -> FitnessCycle.executeMovement("bicepcurl", 10, qiContext)); - Log.i("Motion", "qiContext provided"); + + personalMotionPreviewElement = findViewById(R.id.personalMotionPreviewElement); + + // Initialize the element whenever it has been added to the screen. + // This will provide the element with the appropriate dimensions for drawing + // the canvas properly. + personalMotionPreviewElement.post(() -> { + Exercise exercise = this.acquireExercise(); + if ( exercise == null) { + return; + } + // 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); + + personalMotionPreviewElement.provideQiContext(qiContext); + personalMotionPreviewElement.initialize(exercise, motionProcessor, EXERCISE_COUNT); + + motionProcessor.startListening(); + motionProcessor.setInputHandler(personalMotionPreviewElement); + }); personalMotionPreviewElement.provideQiContext(qiContext); // FitnessCycle.playVideo(qiContext, videoView, this); } + /** + * 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() { + Exercise exercise = ExerciseManager.retrieveExercise(); + if ( exercise == null && this.qiContext != 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); + } + return exercise; + } + @Override public void onRobotFocusLost() { // Implement your logic when the robot focus is lost @@ -94,6 +125,8 @@ public class FitnessActivity extends RobotActivity implements RobotLifecycleCall protected void onDestroy() { super.onDestroy(); QiSDK.unregister(this, this); - this.personalMotionPreviewElement.onDestroy(); + this.motionProcessor.stopListening(); + this.motionProcessor = null; + this.personalMotionPreviewElement.destroy(); } } \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java index 3ffa2e3..cbc714c 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/HelpActivity.java @@ -2,6 +2,7 @@ package com.example.fitbot.ui.activities; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; +import android.view.View; import com.example.fitbot.R; import com.example.fitbot.util.ButtonNavigation; @@ -13,7 +14,18 @@ public class HelpActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_help); - ButtonNavigation.setupButtonNavigation(this, R.id.homeButton, MainActivity.class); + ButtonNavigation.setupButtonNavigation(this, R.id.homeButtonHelp, MainActivity.class); + // Hide system UI + hideSystemUI(); + } + + private void hideSystemUI() { + View decorView = getWindow().getDecorView(); + // Hide the status bar and navigation bar + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + decorView.setSystemUiVisibility(uiOptions); } } \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java index b2fa7ba..4edfa2e 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/activities/MainActivity.java @@ -11,6 +11,8 @@ 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; @@ -18,20 +20,24 @@ import com.example.fitbot.util.ButtonNavigation; public class MainActivity extends AppCompatActivity { - //Variables + // Variables DrawerLayout drawerLayout; NavigationView navigationView; Toolbar toolbar; Button startButton; @SuppressLint("WrongViewCast") - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - Button startButton = findViewById(R.id.startButton); + // Set full screen mode to hide status bar + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + + + startButton = findViewById(R.id.startButtonMain); startButton.setOnClickListener(v -> { Uri videoUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.bicepvideo); Intent intent = new Intent(MainActivity.this, FitnessActivity.class); @@ -39,7 +45,11 @@ public class MainActivity extends AppCompatActivity { startActivity(intent); }); - setUpUi(); // Set up the UI + // Set up the UI + setUpUi(); + + // Hide system UI + hideSystemUI(); } private void setUpUi() { @@ -47,31 +57,65 @@ public class MainActivity extends AppCompatActivity { drawerLayout = findViewById(R.id.drawer_layout); navigationView = findViewById(R.id.nav_view); toolbar = findViewById(R.id.toolbar); - startButton = findViewById(R.id.startButton); + startButton = findViewById(R.id.startButtonMain); - ButtonNavigation.setupButtonNavigation(this, R.id.startButton, FitnessActivity.class); - ButtonNavigation.setupButtonNavigation(this, R.id.helpButton, HelpActivity.class); + // Hide the action bar + if (getSupportActionBar() != null) { + getSupportActionBar().hide(); + } + ButtonNavigation.setupButtonNavigation(this, R.id.startButtonMain, FitnessActivity.class); + ButtonNavigation.setupButtonNavigation(this, R.id.helpButtonMain, HelpActivity.class); /*---Tool Bar---*/ setSupportActionBar(toolbar); // Make the toolbar act as the action bar - getSupportActionBar().setDisplayShowTitleEnabled(false); // Remove the title from the toolbar + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(false); // Remove the title from the toolbar + } /*---Navigation Drawer Menu---*/ navigationView.bringToFront(); // Make the navigation drawer menu clickable - ActionBarDrawerToggle toggle=new // Create a toggle for the navigation drawer - ActionBarDrawerToggle(this,drawerLayout,toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + ActionBarDrawerToggle toggle = new // Create a toggle for the navigation drawer + ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + hideSystemUI(); + } + + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + hideSystemUI(); + } + }; drawerLayout.addDrawerListener(toggle); toggle.syncState(); // Synchronize the state of the navigation drawer } + private void hideSystemUI() { + View decorView = getWindow().getDecorView(); + // Hide the status bar and navigation bar + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + decorView.setSystemUiVisibility(uiOptions); + } + @Override - public void onBackPressed(){ // Close the navigation drawer when the back button is pressed - if(drawerLayout.isDrawerOpen(GravityCompat.START)){ - drawerLayout.closeDrawer(GravityCompat.START); + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + hideSystemUI(); } - else - {super.onBackPressed(); + } + + @Override + public void onBackPressed() { // Close the navigation drawer when the back button is pressed + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); } } } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java index ef41c2e..92f4831 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/ui/components/PersonalMotionPreviewElement.java @@ -1,18 +1,23 @@ 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.graphics.Path; +import android.support.annotation.Nullable; 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.ui.activities.EndScreenActivity; +import com.example.fitbot.ui.activities.FitnessActivity; +import com.example.fitbot.ui.activities.MainActivity; import com.example.fitbot.util.FitnessCycle; -import com.example.fitbot.util.path.GesturePath; -import com.example.fitbot.util.processing.MotionProcessor; +import com.example.fitbot.util.processing.IInputHandler; +import com.example.fitbot.util.processing.InputProcessor; import org.joml.Matrix4f; import org.joml.Vector2f; @@ -20,61 +25,58 @@ import org.joml.Vector3f; import org.joml.Vector4f; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; -public class PersonalMotionPreviewElement extends View { - - private GesturePath[] paths; - private MotionProcessor motionProcessor; - - private double pathTime = 0.0D; // The timestamp at which the path is currently at. - private final AtomicInteger exerciseProgress = new AtomicInteger(0); // The progress of the exercise. Ranges from 0 to 1000. - - private QiContext qiContext; +public class PersonalMotionPreviewElement extends View implements IInputHandler { + // Fields regarding Exercise and speech handling. + private InputProcessor motionProcessor; private Exercise exercise; + private QiContext qiContext; + private int exerciseCount; - private Path targetPath; // The path the user is supposed to follow. - private Path actualPath; // The path the user is currently following. + private FitnessActivity parentActivity; - private final Paint referencePaint = new Paint(); - private final Paint targetPaint = new Paint(); - private final Paint backgroundColor = new Paint(); + private final Paint userProgressPaint = new Paint(); + private final Paint borderPaint = new Paint(); + 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 Vector4f objectPosition = new Vector4f(0, 0, 0, 1); // The location of the object in 3D space. private ConcurrentLinkedQueue vectors = new ConcurrentLinkedQueue<>(); + // TODO: Remove private Vector2f[] axisVectors = new Vector2f[0]; - - private static final String[] USER_PHRASES = { + private static final String[] STARTING_PHRASES = { "Veel success met de oefening!", "Je kan het!", "Veel plezier!" }; - private double timePassed = 0.0D; // The time that has passed since the start of the exercise, in seconds. - private long startingTime = 0L; - - private Vector2f screenDimensions = new Vector2f(); // Width and height dimensions of the screen public PersonalMotionPreviewElement(Context context, AttributeSet attrs) { super(context, attrs); - this.referencePaint.setColor(0xFFFF0000); // Red - this.referencePaint.setStyle(Paint.Style.FILL); - this.referencePaint.setStrokeWidth(5.0f); - this.referencePaint.setAntiAlias(true); + if ( context instanceof Activity) + { + this.parentActivity = (FitnessActivity) context; + } + + this.userProgressPaint.setColor(0xFFFF0000); // Red + this.userProgressPaint.setStyle(Paint.Style.FILL); + this.userProgressPaint.setStrokeWidth(5.0f); + this.userProgressPaint.setAntiAlias(true); // Target paint is the filling of the target path. - this.targetPaint.setColor(-1); - this.targetPaint.setStyle(Paint.Style.STROKE); - this.targetPaint.setStrokeWidth(5.0f); - this.targetPaint.setAntiAlias(true); + this.borderPaint.setColor(-1); + this.borderPaint.setStyle(Paint.Style.STROKE); + this.borderPaint.setStrokeWidth(5.0f); + this.borderPaint.setAntiAlias(true); + + this.backgroundPaint.setColor(0xFF000000); // Black } /** @@ -84,34 +86,28 @@ public class PersonalMotionPreviewElement extends View { * will cause the vertex projections to fail (0 width and height). * * @param exercise The exercise that the user is currently performing. + * @param motionProcessor The motion processor that will be used to process the user's motion. + * @param exerciseCount The total amount of exercises that the user has to perform. */ - public void initialize(Exercise exercise) { + public void initialize(@Nullable Exercise exercise, InputProcessor motionProcessor, int exerciseCount) { Log.i("PersonalMotionPreviewElement", "Creating new PersonalMotionPreviewElement."); - this.backgroundColor.setColor(0xFF000000); // Black - - this.screenDimensions.x = this.getWidth(); - this.screenDimensions.y = this.getHeight(); - this.actualPath = new Path(); - this.targetPath = new Path(); - - this.startingTime = System.nanoTime(); // Set the last time to the current time + this.motionProcessor = motionProcessor; this.exercise = exercise; - this.paths = exercise.getPath(); + this.exerciseCount = exerciseCount; + // TODO: Remove this.axisVectors = new Vector2f[] { - - projectVertex(new Vector3f(-100.0f, 0, 0), getWidth(), getHeight()), - projectVertex(new Vector3f(100.0f, 0, 0), getWidth(), getHeight()), - projectVertex(new Vector3f(0, -100.0f, 0), getWidth(), getHeight()), - projectVertex(new Vector3f(0, 100.0f, 0), getWidth(), getHeight()), - projectVertex(new Vector3f(0, 0, -100.0f), getWidth(), getHeight()), - projectVertex(new Vector3f(0, 0, 100.0f), 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()), + projectVertex(new Vector3f(0, 0, -5.0f), getWidth(), getHeight()), + projectVertex(new Vector3f(0, 0, 5.0f), getWidth(), getHeight()) }; } - public void onDestroy() + public void destroy() { if ( this.motionProcessor != null ) this.motionProcessor.stopListening(); @@ -128,35 +124,55 @@ public class PersonalMotionPreviewElement extends View { */ public void provideQiContext(QiContext context) { this.qiContext = context; - if ( this.motionProcessor != null ) - this.motionProcessor.stopListening(); - - this.motionProcessor = new MotionProcessor(); - this.motionProcessor.startListening(); // Handler that is called every time the motion processor receives new data. - this.motionProcessor.setMotionDataEventHandler((processed, preprocessed, sampleIndex, sampleRate, deviceId) -> { - int progress = (int)this.motionProcessor.getError(this.paths[0], processed); - this.exerciseProgress.set(Math.min(1000, Math.max(0, progress))); - Log.i("MotionProcessor", "Processed data: " + progress + " (" + preprocessed + ")"); - Vector2f parsed = projectVertex(processed, this.getWidth(), this.getHeight()); + 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.parentActivity == null) + { + // Move to main screen + this.destroy(); + Intent intent = new Intent(getContext(), MainActivity.class); + getContext().startActivity(intent); + 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); + } + else + { + // Finish the exercise. + this.destroy(); + Intent intent = new Intent(getContext(), EndScreenActivity.class); + getContext().startActivity(intent); + return; + } + } + + // TODO: Adjust / remove + vectors.add(projectVertex(rotationVector, this.getWidth(), this.getHeight())); + Log.i("MotionProcessor", "Rotation vector received: " + rotationVector); + Vector2f parsed = projectVertex(rotationVector, this.getWidth(), this.getHeight()); + this.vectors.add(parsed); // Remove the first element if the array is too big if (this.vectors.size() > 100) this.vectors.poll(); }); - saySomethingNice(); - } - /** - * Function to say something nice to the user :) - */ - private void saySomethingNice() - { if (this.qiContext == null) return; - FitnessCycle.say(USER_PHRASES[(int) Math.floor(Math.random() * USER_PHRASES.length)], this.qiContext); + FitnessCycle.say(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)], this.qiContext); } /** @@ -165,6 +181,7 @@ public class PersonalMotionPreviewElement extends View { * @param exercise The exercise that the user is currently performing. */ public void setExercise(Exercise exercise) { + this.motionProcessor.useExercise(exercise); this.exercise = exercise; } @@ -178,12 +195,12 @@ public class PersonalMotionPreviewElement extends View { // Perspective transformation conserves the depth of the object projectionMatrix .identity() - .perspective((float) Math.toRadians(70), (float) virtualWidth / virtualHeight, .01f, 10000.0f); + .perspective((float) Math.toRadians(70), (float) virtualWidth / virtualHeight, .01f, 1000.0f); // Convert world coordinates to screen-space using MVP matrix Vector4f screenCoordinates = new Vector4f(point, 1.0f) - .mul(this.projectionMatrix) - .mul(this.viewMatrix); + .mul(this.viewMatrix) + .mul(this.projectionMatrix); // Normalize screen coordinates from (-1, 1) to (0, virtualWidth) and (0, virtualHeight) float normalizedX = (screenCoordinates.x / screenCoordinates.w + 1.0f) * 0.5f * virtualWidth; @@ -195,23 +212,23 @@ public class PersonalMotionPreviewElement extends View { @Override public void onDraw(Canvas canvas) { - canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundColor); + canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint); this.setBackgroundColor(0xFF000000); // Black /*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.screenDimensions.x, (int)axisVectors[i*2].x)); - endX = (int)Math.max(0, Math.min(this.screenDimensions.x, (int)axisVectors[i*2+1].x)); - startY = (int)Math.max(0, Math.min(this.screenDimensions.y, (int)axisVectors[i*2].y)); - endY = (int)Math.max(0, Math.min(this.screenDimensions.y, (int)axisVectors[i*2+1].y)); - canvas.drawLine(startX, startY, endX, endY, this.targetPaint); + 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) { - canvas.drawRect(point.x, point.y, point.x + 5, point.y + 5, this.referencePaint); + canvas.drawRect(point.x, point.y, point.x + 5, point.y + 5, this.userProgressPaint); } /* // Draw target circle @@ -228,7 +245,10 @@ public class PersonalMotionPreviewElement extends View { );*/ this.invalidate(); + } + + @Override + public void accept(Vector3f rotationVector, int sensorId) { - timePassed = (System.nanoTime() - startingTime) / 1E9D; } } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java index 06ebe10..79b7eaf 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/path/GesturePath.java @@ -43,6 +43,19 @@ public class GesturePath { return segments; } + /** + * Method for retrieving the vectors of the GesturePath. + */ + public Vector3f[] getVectors() + { + Vector3f[] vectors = new Vector3f[segments.length + 1]; + vectors[0] = segments[0].getStart(); + for ( int i = 0; i < segments.length; i++) + vectors[i + 1] = segments[i].getEnd(); + + return vectors; + } + /** * Method for retrieving the closest path segment to a reference point. * @@ -71,48 +84,40 @@ public class GesturePath { return closest(referencePoint).difference(referencePoint); // Get the closest segment and calculate the error. } - // Builder class for the GesturePath object. - public static class Builder { - // List of vectors to add to the GesturePath object. - private final List vectors; + /** + * 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. + * + * Note: ASCII to Vector conversion is done in Big Endian format (most significant byte first). + * + * @param input The string to convert + * @return The GesturePath object + */ - /** - * Constructor for the Builder object. - * - * @param vectors The list of vectors to add. - */ - public Builder(List vectors) { - this.vectors = vectors; + public static GesturePath fromString(String input) { + byte[] bytes = input.getBytes(); + + // Check if the input string contains a valid amount of bytes (12 bytes per vector) + if (input.length() % 12 != 0) { + throw new IllegalArgumentException("Invalid input string length"); } + Vector3f[] vectors = new Vector3f[input.length() / 12]; - /** - * Default constructor for the Builder object. - */ - public Builder() { - this.vectors = new ArrayList<>(); + float[] xyz = new float[3]; + for (int i = 0; i < bytes.length; i += 12) { + for (int j = 0; j < 3; j++) { + + xyz[j] = Float.intBitsToFloat( + (bytes[i + j * 4] & 0xFF) << 24 | + (bytes[i + j * 4 + 1] & 0xFF) << 16 | + (bytes[i + j * 4 + 2] & 0xFF) << 8 | + (bytes[i + j * 4 + 3] & 0xFF) + ); + } + vectors[i / 12] = new Vector3f(xyz[0], xyz[1], xyz[2]); } - - /** - * Adds a vector to the GesturePath object. - * - * @param vector The vector to add. - * @return The Builder object. - */ - public Builder addVector(Vector3f vector) { - vectors.add(vector); - return this; - } - - /** - * Builds the GesturePath object. - * - * @return The GesturePath object. - */ - public GesturePath build() { - return new GesturePath(vectors.toArray(new Vector3f[0])); - } - + return new GesturePath(vectors); } - } diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IInputHandler.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IInputHandler.java new file mode 100644 index 0000000..12cb7db --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IInputHandler.java @@ -0,0 +1,14 @@ +package com.example.fitbot.util.processing; + +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); + +} \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IMotionDataConsumer.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IMotionDataConsumer.java deleted file mode 100644 index dd3220c..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/IMotionDataConsumer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.fitbot.util.processing; - -import org.joml.Vector3f; - -public interface IMotionDataConsumer { - - /** - * Function for accepting motion data and the transformed vector. - * @param transformedVector The transformed vector. - * @param motionData The input motion data. - * @param sampleIndex The index of the current sample - * @param sampleRate The sample rate. - */ - void accept(Vector3f transformedVector, MotionData motionData, int sampleIndex, double sampleRate, int sensorId); - -} \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/InputProcessor.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/InputProcessor.java new file mode 100644 index 0000000..dbca4df --- /dev/null +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/InputProcessor.java @@ -0,0 +1,236 @@ +package com.example.fitbot.util.processing; + +import android.util.Log; + +import com.example.fitbot.exercise.Exercise; +import com.example.fitbot.util.server.WebServer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.jetbrains.annotations.NotNull; +import org.joml.Vector3f; + +public class InputProcessor { + + private Vector3f[][] selfRotationVectorPaths; // Relative path of the motion data + private Vector3f[][] targetRotationVectorPaths; // Target path of the motion data + + private final float sampleRate; // The sample rate of the motion sensor + private float exerciseDuration; + + // How many seconds have passed since the start of the exercise. + // The exercise starts whenever the `startListening` function is called. + private double secondsPassed = 0.0D; + private long lastTime; + + private IInputHandler motionDataConsumer = (rotationVector, deviceId) -> { + }; + private WebServer server; + + + /** + * Constructor for the motion processor. + * + * @param paths The target paths of the motion data. + * The length of this array must be equal to the + * amount of sensors available. + * @param inputSampleRate The sample rate of the motion sensor. + */ + public InputProcessor(Vector3f[][] paths, float exerciseTime, float inputSampleRate) { + selfRotationVectorPaths = new Vector3f[paths.length][(int) (exerciseTime * inputSampleRate)]; + targetRotationVectorPaths = paths; + + this.sampleRate = inputSampleRate; + this.exerciseDuration = exerciseTime; + } + + /** + * 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) { + this.selfRotationVectorPaths = new Vector3f[2][(int) (exercise.exerciseTimeInSeconds * this.sampleRate)]; + this.targetRotationVectorPaths = new Vector3f[2][exercise.rightPath.getVectors().length]; + this.exerciseDuration = exercise.exerciseTimeInSeconds; + this.secondsPassed = 0.0D; + this.lastTime = System.currentTimeMillis(); + } + + /** + * Function for checking if the exercise has finished. + * @return True if the exercise has finished, false otherwise. + */ + public boolean hasFinished() { + return this.secondsPassed >= this.exerciseDuration; + } + + /** + * Function for starting the listening process + * of the motion sensor. This function will create + * a new WebSocket server and start listening for + * incoming connections. + */ + public void startListening() { + // Create socket server + this.server = WebServer.createServer(); + + Log.i("MotionProcessor", "Listening for incoming connections."); + + // Check if the socket + if (server != null) { + // Update event handler to match our functionality. + server.setEventHandler(this::parsePacket); + this.secondsPassed = 0.0d; + this.lastTime = System.currentTimeMillis(); + } + } + + /** + * Function for stopping the listening process + * of the motion sensor. This function will stop + * the WebSocket server. + */ + public void stopListening() { + if (server != null) { + server.stop(); + server = null; + } + } + + /** + * Function for parsing arbitrary packet data. + * + * @param data The data to parse. + */ + public void parsePacket(@NotNull String data) { + + try { + + Log.i("MotionProcessor", "Received packet data: " + data); + + JsonElement json = JsonParser.parseString(data); + + if (!json.isJsonObject()) + return; + + JsonObject object = json.getAsJsonObject(); + + String[] required = { + "rotationX", "rotationY", "rotationZ", + "type", + "deviceId" + }; + + // Ensure all properties are present in the received JSON object + for (String s : required) { + if (!object.has(s)) + return; + } + + // Parse the data + Vector3f rotation = new Vector3f(object.get("rotationX").getAsFloat(), object.get("rotationY").getAsFloat(), object.get("rotationZ").getAsFloat()); + int deviceId = object.get("deviceId").getAsInt(); + String type = object.get("type").getAsString(); + + parseRotationVector(rotation, deviceId); + } catch (Exception e) { + Log.i("MotionProcessor", "Failed to parse packet data."); + } + } + + /** + * Function for adding motion data to the processor. + * + * @param rotation The rotation vector of the motion data. + * @param deviceId The device ID of the motion data. + */ + public void parseRotationVector(Vector3f rotation, int deviceId) { + if (deviceId >= 0 && deviceId < selfRotationVectorPaths.length) { + + // Re-calculate time for index calculation + secondsPassed = (System.currentTimeMillis() - lastTime) / 1000.0d; + lastTime = System.currentTimeMillis(); + + // Supposed index of the current rotation vector in the `rotationVectorPaths` array + int selfIndex = (int) (secondsPassed * sampleRate); + + motionDataConsumer.accept(rotation, deviceId); + if (selfIndex >= selfRotationVectorPaths[deviceId].length || selfIndex < 0) + return; + + selfRotationVectorPaths[deviceId][selfIndex] = rotation; + } + } + + /** + * Method for getting the current progress of the exercise. + * The return value will range between 0.0 and 1.0. + * + * @return The current progress of the exercise. + */ + public double getCurrentProgress() + { + return secondsPassed / exerciseDuration; + } + + /** + * Function for setting the motion data receiver. + * + * @param consumer The consumer to set. + */ + public void setInputHandler(IInputHandler consumer) { + if (consumer != null) + this.motionDataConsumer = consumer; + } + + /** + * Function for getting the error offsets of the user's path compared to the + * target path at a given point in time. + * + * @param sensorId The sensor ID to get the error offsets from. + * @param time The time to get the error offsets from. + * This value must be >= 0 && <= exerciseTime, otherwise + * the error will be 0 by default. + * @return A list of error offsets of the motion data compared to the reference path. + */ + public double getError(int sensorId, float time) { + + // Ensure the sensor ID is within the bounds of the array + if (sensorId < 0 || sensorId >= selfRotationVectorPaths.length) + return 0.0d; + + // Index of the current rotation vector + int targetIndex = (int) ((this.exerciseDuration / this.targetRotationVectorPaths[sensorId].length) * time); + int selfIndex = (int) (this.selfRotationVectorPaths[sensorId].length / this.sampleRate * time); + + // 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) + { + return this.selfRotationVectorPaths[sensorId][selfIndex].distance(this.targetRotationVectorPaths[sensorId][targetIndex]); + } + return 0.0d; + } + + /** + * Method for getting the average error of the motion data + * compared to the reference path. + * + * @param sensorId The sensor ID to get the error offsets from. + * @return The average error of the motion data compared to the reference path. + */ + public double getAverageError(int sensorId) { + double error = 0; + for (int i = 0; i < this.exerciseDuration; i++) { + error += getError(sensorId, i); + } + return error / this.exerciseDuration; + } + + public float secondsPassed() { + return (float) secondsPassed; + } +} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionData.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionData.java deleted file mode 100644 index fae42fd..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionData.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.fitbot.util.processing; - -import org.joml.Vector3f; - -import java.util.Objects; - -public class MotionData { - - // Data of the motion sensor - public Vector3f acceleration, rotation; - public int sensorId; - - // Delimiter for the data received from the motion sensor - private static final String DATA_DELIMITER = ";"; - - /** - * Constructor for the MotionData class. - * - * @param accelerationX The acceleration in the X axis in m/s^2. - * @param accelerationY The acceleration in the Y axis in m/s^2. - * @param accelerationZ The acceleration in the Z axis in m/s^2. - * @param rotationX The rotation in the X axis in degrees. - * @param rotationY The rotation in the Y axis in degrees. - * @param rotationZ The rotation in the Z axis in degrees. - * @param sensorId The sensor id. - */ - public MotionData(float accelerationX, float accelerationY, float accelerationZ, float rotationX, float rotationY, float rotationZ, int sensorId) { - this(new Vector3f(accelerationX, accelerationY, accelerationZ), new Vector3f(rotationX, rotationY, rotationZ), sensorId); - } - - /** - * Constructor for the MotionData class. - * - * @param acceleration The acceleration vector in m/s^2. - * @param rotation The rotation vector in degrees. - */ - public MotionData(Vector3f acceleration, Vector3f rotation, int sensorId) { - this.acceleration = acceleration; - this.rotation = rotation; - this.sensorId = sensorId; - } - - /** - * Function for decoding a string into a MotionData object. - * This string must contain the data of the motion sensor - * separated by the delimiter. (;) - * - * @param data The string containing the data of the motion sensor. - * @return An instance of MotionData. - */ - public static MotionData decode(String data) { - Objects.requireNonNull(data); // Ensure data is not null - - String[] parts = data.split(DATA_DELIMITER); - if (parts.length != 7) - return null; - - return new MotionData( - Float.parseFloat(parts[0]), - Float.parseFloat(parts[1]), - Float.parseFloat(parts[2]), - Float.parseFloat(parts[3]), - Float.parseFloat(parts[4]), - Float.parseFloat(parts[5]), - Integer.parseInt(parts[6]) - ); - } -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java deleted file mode 100644 index de35e7d..0000000 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/processing/MotionProcessor.java +++ /dev/null @@ -1,233 +0,0 @@ -package com.example.fitbot.util.processing; - -import android.util.Log; - -import com.example.fitbot.util.path.GesturePath; -import com.example.fitbot.util.server.WebServer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -import org.jetbrains.annotations.NotNull; -import org.joml.Matrix3d; -import org.joml.Vector3d; -import org.joml.Vector3f; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -public class MotionProcessor { - - public static final String DELIMITER = ";"; - - private final List relativeLeftPath = new ArrayList<>(); // Relative path of the left motion data - private final List relativeRightPath = new ArrayList<>(); // Relative path of the motion data - - private Vector3f ZERO = new Vector3f(0, 0, 0); - - private final float sampleRate = 1.0f / 10.0F; // samples/second - - private IMotionDataConsumer motionDataConsumer = (p1, p2, p3, p4, p5) -> { }; - private WebServer server; - - - public MotionProcessor() {} - - - /** - * Function for starting the listening process - * of the motion sensor. This function will create - * a new WebSocket server and start listening for - * incoming connections. - */ - public void startListening() { - // Create socket server - this.server = WebServer.createServer(); - - Log.i("MotionProcessor", "Listening for incoming connections."); - - // Check if the socket - if (server != null) { - // Update event handler to match our functionality. - server.setEventHandler(this::parsePacket); - } - } - - /** - * Function for stopping the listening process - * of the motion sensor. This function will stop - * the WebSocket server. - */ - public void stopListening() { - if (server != null) { - server.stop(); - } - } - - /** - * Function for parsing arbitrary packet data. - * - * @param data The data to parse. - */ - public void parsePacket(@NotNull String data) { - - try { - - Log.i("MotionProcessor", "Received packet data: " + data); - - JsonElement json = JsonParser.parseString(data); - - if (!json.isJsonObject()) - return; - - JsonObject object = json.getAsJsonObject(); - - String[] required = { - "rotationX", "rotationY", "rotationZ", - "accelerationX", "accelerationY", "accelerationZ", - "type", - "deviceId" - }; - - // Ensure all properties are present in the received JSON object - for (String s : required) { - if (!object.has(s)) - return; - } - - // Parse the data - Vector3f rotation = new Vector3f(object.get("rotationX").getAsFloat(), object.get("rotationY").getAsFloat(), object.get("rotationZ").getAsFloat()); - Vector3f acceleration = new Vector3f(object.get("accelerationX").getAsFloat(), object.get("accelerationY").getAsFloat(), object.get("accelerationZ").getAsFloat()); - int deviceId = object.get("deviceId").getAsInt(); - String type = object.get("type").getAsString(); - MotionData motionData = new MotionData(rotation, acceleration, deviceId); - - if (type.equals("calibrate")) { - ZERO = getRelativeVector(motionData); - return; - } - - addMotionData(motionData); - } catch (Exception e) { - Log.i("MotionProcessor", "Failed to parse packet data."); - } - } - - /** - * Function for adding motion data to the processor. - * - * @param data The motion data to add. - */ - public void addMotionData(MotionData data) { - List target; - if (data.sensorId == 0) - target = relativeLeftPath; - else target = relativeRightPath; - Vector3f previous = target.isEmpty() ? ZERO : target.get(target.size() - 1); - Vector3f relativeVector = getRelativeVector(data).add(previous); - target.add(relativeVector); - motionDataConsumer.accept(relativeVector, data, target.size(), this.sampleRate, data.sensorId); - } - - /** - * Function for updating the relative path. - * - * @param relativeRightPath The new relative path. - */ - public void setRelativePaths(List relativeLeftPath, List relativeRightPath) { - this.relativeRightPath.clear(); - this.relativeLeftPath.clear(); - this.relativeLeftPath.addAll(relativeLeftPath); - this.relativeRightPath.addAll(relativeRightPath); - } - - /** - * Function for setting the motion data receiver. - * - * @param consumer The consumer to set. - */ - public void setMotionDataEventHandler(IMotionDataConsumer consumer) { - if (consumer != null) - this.motionDataConsumer = consumer; - } - - /** - * Function for getting the relative vector of the motion data. - * This function will calculate the relative position of the motion data - * based on its acceleration and rotation vectors. This has to be done since - * the acceleration vector is relative to its own rotation vector. - * - * @param motionData The motion data to calculate the relative vector for. - * @return The relative vector of the motion data. - */ - public Vector3f getRelativeVector(MotionData motionData) { - - // Rotate the acceleration vector back by the rotation vector to make it - // perpendicular to the gravity vector, then apply double integration to get the relative position. - // s = 1/2 * a * t^2 - // Step 2: Create rotation matrices for each axis - // Step 4: Rotate the acceleration vector - - return motionData.rotation - .mul(5); - /*return motionData.acceleration - .rotateZ(-motionData.rotation.z) - .rotateY(-motionData.rotation.y) - .rotateX(-motionData.rotation.x) - .mul(sampleRate * sampleRate / 2);*/ - } - - /** - * Function for getting the error offsets of the provided path and the - * received motion data. - * - * @param referencePath The reference path to compare the motion data to. - * @return A list of error offsets of the motion data compared to the reference path. - */ - public List getErrors(GesturePath referencePath) { - - List errors = new ArrayList<>(); - for (Vector3f vector : relativeRightPath) { - errors.add(referencePath.getError(vector)); - } - return errors; - } - - /** - * Function for getting the error of the motion data compared to the reference path. - * - * @param path The path to compare the motion data to. - * @param referencePoint The reference point to compare the motion data to. - * @return The error of the motion data compared to the reference path. - */ - public double getError(GesturePath path, Vector3f referencePoint) { - return path.getError(referencePoint); - } - - /** - * Function for calculating the average error of the motion data - * compared to the reference path. - * - * @param referencePath The reference path to compare the motion data to. - * @return The average error of the motion data compared to the reference path. - */ - public double getAverageError(GesturePath referencePath, int sensorId) { - double error = 0; - for (Double e : getErrors(referencePath)) { - error += e; - } - return error / Math.max(1, (sensorId == 0 ? relativeLeftPath : relativeRightPath).size()); - } - - /** - * Function for logging the statistics of the motion data. - * - * @param referencePath The reference path to compare the motion data to. - */ - public void logStatistics(GesturePath referencePath) { - Log.i("MotionProcessor", "Path length: " + relativeRightPath.size()); - Log.i("MotionProcessor", "Sample rate: " + sampleRate); - Log.i("MotionProcessor", "Calibration point: " + ZERO.toString()); - } -} diff --git a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebServer.java b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebServer.java index 7fe001a..6eaf4dc 100644 --- a/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebServer.java +++ b/code/src/Fitbot/app/src/main/java/com/example/fitbot/util/server/WebServer.java @@ -22,7 +22,7 @@ public class WebServer implements Runnable { protected IWebServerHandler eventHandler = (input) -> {}; // No-op. private Thread thread; - private AtomicBoolean forceClose = new AtomicBoolean(false); + private final AtomicBoolean forceClose = new AtomicBoolean(false); /** * Constructor for creating a new WebSocket server. diff --git a/code/src/Fitbot/app/src/main/res/drawable/darkred_button_gradient.xml b/code/src/Fitbot/app/src/main/res/drawable/big_red_button_gradient.xml similarity index 63% rename from code/src/Fitbot/app/src/main/res/drawable/darkred_button_gradient.xml rename to code/src/Fitbot/app/src/main/res/drawable/big_red_button_gradient.xml index 0ade340..878f41e 100644 --- a/code/src/Fitbot/app/src/main/res/drawable/darkred_button_gradient.xml +++ b/code/src/Fitbot/app/src/main/res/drawable/big_red_button_gradient.xml @@ -2,11 +2,12 @@ + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/box_background.xml b/code/src/Fitbot/app/src/main/res/drawable/box_background.xml new file mode 100644 index 0000000..53f8ad7 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/box_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/help2_background.xml b/code/src/Fitbot/app/src/main/res/drawable/help2_background.xml new file mode 100644 index 0000000..0fb955b --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/help2_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/help_background.xml b/code/src/Fitbot/app/src/main/res/drawable/help_background.xml new file mode 100644 index 0000000..7ea69e6 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/help_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_check_48.xml b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_check_48.xml new file mode 100644 index 0000000..c4f67b4 --- /dev/null +++ b/code/src/Fitbot/app/src/main/res/drawable/ic_baseline_check_48.xml @@ -0,0 +1,5 @@ + + + diff --git a/code/src/Fitbot/app/src/main/res/drawable/rectangle.xml b/code/src/Fitbot/app/src/main/res/drawable/rectangle.xml deleted file mode 100644 index 20ab824..0000000 --- a/code/src/Fitbot/app/src/main/res/drawable/rectangle.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml b/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml index 7190f99..6983a91 100644 --- a/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml +++ b/code/src/Fitbot/app/src/main/res/drawable/red_button_gradient.xml @@ -2,11 +2,12 @@ + \ No newline at end of file diff --git a/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml b/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml index b07bcad..4579826 100644 --- a/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml +++ b/code/src/Fitbot/app/src/main/res/layout/activity_end_screen.xml @@ -1,77 +1,113 @@ + android:background="@color/darkBlue" + tools:context=".ui.activities.HelpActivity"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + +