Implement handling exercise timer and statistics.
authorPiotr Czaja/Advanced Frameworks (PLT) /SRPOL/Engineer/Samsung Electronics <p.czaja@samsung.com>
Thu, 22 Jul 2021 09:01:27 +0000 (11:01 +0200)
committerPiotr Czaja <p.czaja@samsung.com>
Tue, 14 Sep 2021 11:01:34 +0000 (13:01 +0200)
Fitness/Services/Exercises/BaseExerciseService.cs
Fitness/Services/Exercises/ExerciseEventArgs.cs [deleted file]
Fitness/Services/Exercises/ExerciseStateUpdatedEventArgs.cs [new file with mode: 0644]
Fitness/Services/Exercises/IExerciseService.cs
Fitness/Services/Exercises/SquatService.cs
Fitness/Services/Exercises/TimeLeftChangedEventArgs.cs [new file with mode: 0644]
Fitness/Services/WorkoutRepository.cs
Fitness/ViewModels/ExercisingViewModel.cs
Fitness/ViewModels/ScanningViewModel.cs
Fitness/Views/ExercisingView.xaml.cs

index 0c727532b7d7a5d5b210b9d014d27d7319bae5b2..b0f162666e08cde3ee01ce0516338a22e34b503c 100644 (file)
@@ -8,6 +8,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Tizen.Multimedia;
 using Tizen.Multimedia.Vision;
+using Tizen.NUI;
 
 namespace Fitness.Services
 {
@@ -18,6 +19,44 @@ namespace Fitness.Services
     {
         private readonly PoseDetector poseDetector = new PoseDetector();
         private int isInferencing = 0;
+        private TimeSpan timeLeft;
+        private TimeSpan duration;
+        private Tizen.NUI.Timer timer;
+        private bool timerConfigured = false;
+        private bool workoutStopped = false;
+
+        /// <summary>
+        /// Event raised when workout time left has changed.
+        /// </summary>
+        public event EventHandler<TimeLeftChangedEventArgs> TimeLeftChanged;
+
+        /// <summary>
+        /// Event raised when workout time is up.
+        /// </summary>
+        public event EventHandler WorkoutTimeEnded;
+
+        /// <summary>
+        /// Gets or sets the duration of the workout.
+        /// </summary>
+        public TimeSpan Duration
+        {
+            get => duration;
+            set => duration = value;
+        }
+
+        /// <summary>
+        /// Gets the time remaining until the end of the exercise.
+        /// </summary>
+        public TimeSpan TimeLeft => timeLeft;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether timer has been configured.
+        /// </summary>
+        protected bool TimerConfigured
+        {
+            get => timerConfigured;
+            set => timerConfigured = value;
+        }
 
         /// <summary>
         /// Detects performing the exercise based on given preview image data.
@@ -45,6 +84,10 @@ namespace Fitness.Services
                     var landmarks = await poseDetector.Detect(plane, height, width);
 
                     DetectExercise(landmarks);
+                    if (workoutStopped)
+                    {
+                        return;
+                    }
                 }
             }
             catch (System.Exception exception)
@@ -60,10 +103,58 @@ namespace Fitness.Services
             }
         }
 
+        /// <summary>
+        /// Starts workout.
+        /// </summary>
+        public void StartWorkout()
+        {
+            if (!TimerConfigured)
+            {
+                ConfigureTimer();
+            }
+
+            timer?.Start();
+            workoutStopped = false;
+        }
+
+        /// <summary>
+        /// Stops workout.
+        /// </summary>
+        public void StopWorkout()
+        {
+            timer?.Stop();
+            workoutStopped = true;
+        }
+
         /// <summary>
         /// Detects performing the exercise based on given landmarks.
         /// </summary>
         /// <param name="landmarks">Body landmarks.</param>
         protected abstract void DetectExercise(Landmark[,] landmarks);
+
+        private void ConfigureTimer()
+        {
+            timer = new Tizen.NUI.Timer(1000);
+            timeLeft = Duration;
+
+            timer.Tick += (sender, args) =>
+            {
+                if (timeLeft.CompareTo(TimeSpan.FromSeconds(0)) <= 0)
+                {
+                    StopWorkout();
+
+                    WorkoutTimeEnded?.Invoke(this, EventArgs.Empty);
+                    return true;
+                }
+
+                timeLeft = timeLeft.Subtract(TimeSpan.FromSeconds(1));
+                TimeLeftChanged?.Invoke(this, new TimeLeftChangedEventArgs()
+                {
+                    TimeLeft = timeLeft,
+                });
+                return true;
+            };
+            TimerConfigured = true;
+        }
     }
 }
diff --git a/Fitness/Services/Exercises/ExerciseEventArgs.cs b/Fitness/Services/Exercises/ExerciseEventArgs.cs
deleted file mode 100644 (file)
index 48b806b..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Tizen.Multimedia.Vision;
-
-namespace Fitness.Services
-{
-    /// <summary>
-    /// Class holding exercise event data.
-    /// </summary>
-    public class ExerciseEventArgs : EventArgs
-    {
-        /// <summary>
-        /// Gets the pose landmark property.
-        /// </summary>
-        public Landmark[,] PoseLandmarks { get; internal set; }
-
-        /// <summary>
-        /// Gets the property specifying the time of the exercise performed - values ranging from 0 to 5.
-        /// </summary>
-        public int Hold { get; internal set; }
-
-        /// <summary>
-        /// Gets the property specifying the number of repetitions of the exercise.
-        /// </summary>
-        public int Count { get; internal set; }
-
-        /// <summary>
-        /// Gets the property specifying the correctness of the exercise - values ranging from 0 to 100.
-        /// </summary>
-        public int Score { get; internal set; }
-    }
-}
diff --git a/Fitness/Services/Exercises/ExerciseStateUpdatedEventArgs.cs b/Fitness/Services/Exercises/ExerciseStateUpdatedEventArgs.cs
new file mode 100644 (file)
index 0000000..aa9f4c6
--- /dev/null
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.Multimedia.Vision;
+
+namespace Fitness.Services
+{
+    /// <summary>
+    /// The class that stores the exercise updated event data.
+    /// </summary>
+    public class ExerciseStateUpdatedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets the pose landmark property.
+        /// </summary>
+        public Landmark[,] PoseLandmarks { get; internal set; }
+
+        /// <summary>
+        /// Gets the property specifying the time of the exercise performed - values ranging from 0 to 5.
+        /// </summary>
+        public int Hold { get; internal set; }
+
+        /// <summary>
+        /// Gets the property specifying the number of repetitions of the exercise.
+        /// </summary>
+        public int Count { get; internal set; }
+
+        /// <summary>
+        /// Gets the property specifying the correctness of the exercise - values ranging from 0 to 100.
+        /// </summary>
+        public int Score { get; internal set; }
+    }
+}
index 146d6fdd2a45c17d41eb92834828bcf0a9bdce1c..9dde113f9d34d05298f7d498a8e7cb40299bfcf7 100644 (file)
@@ -16,13 +16,58 @@ namespace Fitness.Services
         /// <summary>
         /// Event raised when exercise status is updated.
         /// </summary>
-        event EventHandler<ExerciseEventArgs> ExerciseStateUpdated;
+        event EventHandler<ExerciseStateUpdatedEventArgs> ExerciseStateUpdated;
+
+        /// <summary>
+        /// Event raised when workout time left has changed.
+        /// </summary>
+        event EventHandler<TimeLeftChangedEventArgs> TimeLeftChanged;
+
+        /// <summary>
+        /// Event raised when workout time is up.
+        /// </summary>
+        event EventHandler WorkoutTimeEnded;
+
+        /// <summary>
+        /// Gets or sets the duration of the workout.
+        /// </summary>
+        TimeSpan Duration { get; set; }
 
         /// <summary>
         /// Gets or sets the hold time threshold specifying the time of a single exercise.
         /// </summary>
         long HoldTimeThreshold { get; set; }
 
+        /// <summary>
+        /// Gets the property specifying the average score above the score threshold specified for the exercise.
+        /// </summary>
+        int AverageScore { get; }
+
+        /// <summary>
+        /// Gets the property specifying the number of repetitions of the exercise.
+        /// </summary>
+        int Count { get; }
+
+        /// <summary>
+        /// Gets the time remaining until the end of the exercise.
+        /// </summary>
+        TimeSpan TimeLeft { get; }
+
+        /// <summary>
+        /// Starts workout.
+        /// </summary>
+        void StartWorkout();
+
+        /// <summary>
+        /// Stops workout.
+        /// </summary>
+        void StopWorkout();
+
+        /// <summary>
+        /// Resets workout.
+        /// </summary>
+        void ResetWorkout();
+
         /// <summary>
         /// Detects performing the exercise based on given preview image data.
         /// </summary>
index 084ebcc0a884b9c8ab4c34fec89a0d518c76b06a..5d296ef5f388f6e0bbea34248a88a7e91d077d21 100644 (file)
@@ -1,5 +1,5 @@
-using System;
-using System.ComponentModel;
+using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Timers;
@@ -18,12 +18,26 @@ namespace Fitness.Services
         private int count;
         private int score;
         private float currentSquatSimilarity = 0;
+        private int averageScore;
         private Stopwatch stopwatch = new Stopwatch();
         private System.Timers.Timer holdTimer;
         private long holdTimeThreshold;
+        private List<int> allScores = new List<int>();
 
         /// <inheritdoc />
-        public event EventHandler<ExerciseEventArgs> ExerciseStateUpdated;
+        public event EventHandler<ExerciseStateUpdatedEventArgs> ExerciseStateUpdated;
+
+        /// <inheritdoc />
+        public int AverageScore
+        {
+            get => averageScore;
+        }
+
+        /// <inheritdoc />
+        public int Count
+        {
+            get => count;
+        }
 
         /// <inheritdoc />
         public long HoldTimeThreshold
@@ -39,6 +53,14 @@ namespace Fitness.Services
             }
         }
 
+        /// <inheritdoc />
+        public void ResetWorkout()
+        {
+            TimerConfigured = false;
+            count = 0;
+            allScores.Clear();
+        }
+
         /// <inheritdoc />
         protected override void DetectExercise(Landmark[,] landmarks)
         {
@@ -56,13 +78,14 @@ namespace Fitness.Services
 
             NUIContext.InvokeOnMainThread(() =>
             {
-                ExerciseStateUpdated.Invoke(this, new ExerciseEventArgs()
+                ExerciseStateUpdated?.Invoke(this, new ExerciseStateUpdatedEventArgs()
                 {
                     PoseLandmarks = landmarks,
                     Hold = hold,
                     Count = count,
                     Score = score,
                 });
+                averageScore = (int)System.Math.Round((float)(allScores.Count > 0 ? allScores.Average() : 0));
             });
         }
 
@@ -110,6 +133,11 @@ namespace Fitness.Services
                 hold = 0;
                 stopwatch.Reset();
             }
+
+            if (currentSquatSimilarity >= SquatDetectedThreshold)
+            {
+                allScores.Add(score);
+            }
         }
     }
 }
diff --git a/Fitness/Services/Exercises/TimeLeftChangedEventArgs.cs b/Fitness/Services/Exercises/TimeLeftChangedEventArgs.cs
new file mode 100644 (file)
index 0000000..dedba1b
--- /dev/null
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Fitness.Services
+{
+    /// <summary>
+    /// The class that stores the exercise time left data.
+    /// </summary>
+    public class TimeLeftChangedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the time remaining until the end of the exercise.
+        /// </summary>
+        public TimeSpan TimeLeft { get; internal set; }
+    }
+}
index 4a2d98fa31f3b7ca64179b5e3ff608c9de2babbc..8551f926bc1f3b13342255b5252549832b916f5f 100644 (file)
@@ -21,7 +21,7 @@ namespace Fitness.Services
                 Title = "JOGA Workout 0",
                 Description = "1. Lie down on your back, keep your knees bent and your back and feet flat on the mat.\n2. Slowly lift your torso and situp.\n3. Return to the starting position by rolling down one vertebrae at a time.\n4. Repeat the exercise until set is complete.",
                 Difficulty = DifficultyLevel.Easy,
-                Duration = new TimeSpan(0, 4, 30),
+                Duration = new TimeSpan(0, 0, 20),
                 Favourite = true,
                 VideoUrl = Application.Current.DirectoryInfo.Resource + "media/JOGA-0000.avi",
                 ThumbnailUrl = Application.Current.DirectoryInfo.Resource + "media/JOGA-0000.jpeg",
index 6684f84b5f7adc9192c46b38ed23a6bcb4672d36..b1bd4d8dfb89e87b116ccc4545631e272f68eb92 100644 (file)
@@ -28,6 +28,10 @@ namespace Fitness.ViewModels
         private ICommand summaryBackCommand;
         private ICommand summaryOkCommand;
         private string summaryTitle;
+        private TimeSpan timeLeft;
+        private int totatCount;
+        private TimeSpan totalTime;
+        private int avarageScore;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ExercisingViewModel"/> class.
@@ -39,11 +43,15 @@ namespace Fitness.ViewModels
             TryAgain = new Command(ConfirmTryAgain);
             EndWorkout = new Command(ConfirmEndWorkout);
             CurrentWorkout = workoutViewModel;
+            TimeLeft = workoutViewModel.Duration;
             SquatService = new SquatService()
             {
                 HoldTimeThreshold = 1200,
+                Duration = CurrentWorkout.Duration,
             };
-            SquatService.ExerciseStateUpdated += SquatService_ExerciseStateUpdated;
+            SquatService.ExerciseStateUpdated += OnSquatServiceExerciseStateUpdated;
+            SquatService.WorkoutTimeEnded += OnSquatServiceWorkoutTimeEnded;
+            SquatService.TimeLeftChanged += OnSquatServiceTimeLeftChanged;
         }
 
         /// <summary>
@@ -113,12 +121,50 @@ namespace Fitness.ViewModels
         /// <summary>
         /// Gets AverageScore.
         /// </summary>
-        public int AverageScore { get; private set; } = 98;
+        public int AverageScore
+        {
+            get => avarageScore;
+            private set
+            {
+                if (value != avarageScore)
+                {
+                    avarageScore = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets TotalTime.
+        /// </summary>
+        public TimeSpan TotalTime
+        {
+            get => totalTime;
+            private set
+            {
+                if (value != totalTime)
+                {
+                    totalTime = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
 
         /// <summary>
         /// Gets TotalCount.
         /// </summary>
-        public int TotalCount { get; private set; } = 27;
+        public int TotalCount
+        {
+            get => totatCount;
+            private set
+            {
+                if (value != totatCount)
+                {
+                    totatCount = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
 
         /// <summary>
         /// Gets the property specifying the correctness of the exercise - values ranging from 0 to 100.
@@ -169,14 +215,20 @@ namespace Fitness.ViewModels
         }
 
         /// <summary>
-        /// Gets TotalTime.
+        /// Gets the time remaining until the end of the exercise.
         /// </summary>
-        public TimeSpan TotalTime { get; private set; } = new TimeSpan(0, 3, 39);
-
-        /// <summary>
-        /// Gets the TimeLeft in workout.
-        /// </summary>
-        public TimeSpan TimeLeft { get; private set; } = TimeSpan.FromSeconds(27);
+        public TimeSpan TimeLeft
+        {
+            get => timeLeft;
+            private set
+            {
+                if (timeLeft != value)
+                {
+                    timeLeft = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
 
         /// <summary>
         /// Gets or sets the current workout state.
@@ -274,6 +326,7 @@ namespace Fitness.ViewModels
 
         private void ConfirmChangeWorkout(int offset)
         {
+            UpdateStatistics();
             if (State == WorkoutState.Playing)
             {
                 State = WorkoutState.OnHold;
@@ -313,6 +366,9 @@ namespace Fitness.ViewModels
 
         private void StartNewWorkout()
         {
+            TimeLeft = CurrentWorkout.Duration;
+            SquatService.Duration = CurrentWorkout.Duration;
+            SquatService.ResetWorkout();
             State = WorkoutState.Loading;
             IsSummaryVisible = false;
         }
@@ -335,7 +391,7 @@ namespace Fitness.ViewModels
             }
         }
 
-        private void SquatService_ExerciseStateUpdated(object sender, ExerciseEventArgs e)
+        private void OnSquatServiceExerciseStateUpdated(object sender, ExerciseStateUpdatedEventArgs e)
         {
             PoseLandmarks = e.PoseLandmarks;
             Score = e.Score;
@@ -343,6 +399,16 @@ namespace Fitness.ViewModels
             Hold = e.Hold;
         }
 
+        private void OnSquatServiceWorkoutTimeEnded(object sender, EventArgs e)
+        {
+            ConfirmTimeIsUp();
+        }
+
+        private void OnSquatServiceTimeLeftChanged(object sender, TimeLeftChangedEventArgs e)
+        {
+            TimeLeft = e.TimeLeft;
+        }
+
         private void TriggerPauseResumeWorkout()
         {
             if (State == WorkoutState.Paused)
@@ -357,6 +423,7 @@ namespace Fitness.ViewModels
 
         private void ConfirmTryAgain()
         {
+            UpdateStatistics();
             SummaryBackCommand = new Command(ExecuteCloseSummary);
             SummaryOkCommand = new Command(() => { ExecuteChangeWorkout(); });
             SummaryTitle = GetSummaryTitle(SummaryType.TryAgain);
@@ -365,10 +432,26 @@ namespace Fitness.ViewModels
 
         private void ConfirmEndWorkout()
         {
+            UpdateStatistics();
             SummaryBackCommand = new Command(ExecuteCloseSummary);
             SummaryOkCommand = new Command(Services.NavigationService.Instance.PopToRoot);
             SummaryTitle = GetSummaryTitle(SummaryType.EndWorkout);
             IsSummaryVisible = true;
         }
+
+        private void ConfirmTimeIsUp()
+        {
+            UpdateStatistics();
+            SummaryOkCommand = new Command(Services.NavigationService.Instance.PopToRoot);
+            SummaryTitle = GetSummaryTitle(SummaryType.TimeIsUp);
+            IsSummaryVisible = true;
+        }
+
+        private void UpdateStatistics()
+        {
+            TotalCount = SquatService.Count;
+            AverageScore = SquatService.AverageScore;
+            TotalTime = SquatService.Duration.Subtract(SquatService.TimeLeft);
+        }
     }
 }
index 690815d2a921eaa67b5c43261abdc7f281710023..089bf988462a6e4750053c75b66460982c247003 100644 (file)
@@ -110,7 +110,7 @@ namespace Fitness.ViewModels
         /// </summary>
         public ICommand CloseScanningView { get; private set; }
 
-        private void SquatService_ExerciseStateUpdated(object sender, ExerciseEventArgs e)
+        private void SquatService_ExerciseStateUpdated(object sender, ExerciseStateUpdatedEventArgs e)
         {
             PoseLandmarks = e.PoseLandmarks;
             Score = e.Score;
index a81eadca1cdcdaffb1510c94d2b43bed8782d5c0..bb55f45adc8d7911fa05ea38762f73e2e3955483 100644 (file)
@@ -201,7 +201,9 @@ namespace Fitness.Views
             switch (oldState)
             {
                 case WorkoutState.Loading:
+                    break;
                 case WorkoutState.OnHold:
+                    StartWorkout();
                     break;
 
                 case WorkoutState.Paused:
@@ -216,6 +218,7 @@ namespace Fitness.Views
                     if (!source.IsCancellationRequested)
                     {
                         SetPositionAndSize(WorkoutState.Playing);
+                        StartWorkout();
                     }
 
                     break;
@@ -234,6 +237,7 @@ namespace Fitness.Views
             {
                 case WorkoutState.Playing:
                     Preview.Pause();
+                    StopWorkout();
                     break;
 
                 default:
@@ -251,6 +255,7 @@ namespace Fitness.Views
                     Preview.Pause();
                     PlayingView.Hide();
                     PauseView.Show();
+                    StopWorkout();
 
                     (Task scalePreview, Task movePreview) = Animate(Preview, playing.Preview, pause.Preview, source.Token);
                     (Task scaleCamera, Task moveCamera) = Animate(cameraOverlayView, playing.Camera, pause.Camera, source.Token);
@@ -285,6 +290,23 @@ namespace Fitness.Views
             }
 
             LoadingView.Loaded -= OnLoaded;
+            StartWorkout();
+        }
+
+        private void StartWorkout()
+        {
+            if (BindingContext is ViewModels.ExercisingViewModel viewModel)
+            {
+                viewModel.SquatService.StartWorkout();
+            }
+        }
+
+        private void StopWorkout()
+        {
+            if (BindingContext is ViewModels.ExercisingViewModel viewModel)
+            {
+                viewModel.SquatService.StopWorkout();
+            }
         }
 
         private void SetPositionAndSize()