From 576ce9037a380aa37529b6bc2893ef0fe1b4718e Mon Sep 17 00:00:00 2001 From: "Piotr Czaja/Advanced Frameworks (PLT) /SRPOL/Engineer/Samsung Electronics" Date: Thu, 22 Jul 2021 11:01:27 +0200 Subject: [PATCH] Implement handling exercise timer and statistics. --- Fitness/Services/Exercises/BaseExerciseService.cs | 91 ++++++++++++++++++ ...entArgs.cs => ExerciseStateUpdatedEventArgs.cs} | 4 +- Fitness/Services/Exercises/IExerciseService.cs | 47 ++++++++- Fitness/Services/Exercises/SquatService.cs | 36 ++++++- .../Services/Exercises/TimeLeftChangedEventArgs.cs | 19 ++++ Fitness/Services/WorkoutRepository.cs | 2 +- Fitness/ViewModels/ExercisingViewModel.cs | 105 ++++++++++++++++++--- Fitness/ViewModels/ScanningViewModel.cs | 2 +- Fitness/Views/ExercisingView.xaml.cs | 22 +++++ 9 files changed, 308 insertions(+), 20 deletions(-) rename Fitness/Services/Exercises/{ExerciseEventArgs.cs => ExerciseStateUpdatedEventArgs.cs} (88%) create mode 100644 Fitness/Services/Exercises/TimeLeftChangedEventArgs.cs diff --git a/Fitness/Services/Exercises/BaseExerciseService.cs b/Fitness/Services/Exercises/BaseExerciseService.cs index 0c72753..b0f1626 100644 --- a/Fitness/Services/Exercises/BaseExerciseService.cs +++ b/Fitness/Services/Exercises/BaseExerciseService.cs @@ -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; + + /// + /// Event raised when workout time left has changed. + /// + public event EventHandler TimeLeftChanged; + + /// + /// Event raised when workout time is up. + /// + public event EventHandler WorkoutTimeEnded; + + /// + /// Gets or sets the duration of the workout. + /// + public TimeSpan Duration + { + get => duration; + set => duration = value; + } + + /// + /// Gets the time remaining until the end of the exercise. + /// + public TimeSpan TimeLeft => timeLeft; + + /// + /// Gets or sets a value indicating whether timer has been configured. + /// + protected bool TimerConfigured + { + get => timerConfigured; + set => timerConfigured = value; + } /// /// 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) @@ -61,9 +104,57 @@ namespace Fitness.Services } /// + /// Starts workout. + /// + public void StartWorkout() + { + if (!TimerConfigured) + { + ConfigureTimer(); + } + + timer?.Start(); + workoutStopped = false; + } + + /// + /// Stops workout. + /// + public void StopWorkout() + { + timer?.Stop(); + workoutStopped = true; + } + + /// /// Detects performing the exercise based on given landmarks. /// /// Body landmarks. 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/ExerciseStateUpdatedEventArgs.cs similarity index 88% rename from Fitness/Services/Exercises/ExerciseEventArgs.cs rename to Fitness/Services/Exercises/ExerciseStateUpdatedEventArgs.cs index 48b806b..aa9f4c6 100644 --- a/Fitness/Services/Exercises/ExerciseEventArgs.cs +++ b/Fitness/Services/Exercises/ExerciseStateUpdatedEventArgs.cs @@ -8,9 +8,9 @@ using Tizen.Multimedia.Vision; namespace Fitness.Services { /// - /// Class holding exercise event data. + /// The class that stores the exercise updated event data. /// - public class ExerciseEventArgs : EventArgs + public class ExerciseStateUpdatedEventArgs : EventArgs { /// /// Gets the pose landmark property. diff --git a/Fitness/Services/Exercises/IExerciseService.cs b/Fitness/Services/Exercises/IExerciseService.cs index 146d6fd..9dde113 100644 --- a/Fitness/Services/Exercises/IExerciseService.cs +++ b/Fitness/Services/Exercises/IExerciseService.cs @@ -16,7 +16,22 @@ namespace Fitness.Services /// /// Event raised when exercise status is updated. /// - event EventHandler ExerciseStateUpdated; + event EventHandler ExerciseStateUpdated; + + /// + /// Event raised when workout time left has changed. + /// + event EventHandler TimeLeftChanged; + + /// + /// Event raised when workout time is up. + /// + event EventHandler WorkoutTimeEnded; + + /// + /// Gets or sets the duration of the workout. + /// + TimeSpan Duration { get; set; } /// /// Gets or sets the hold time threshold specifying the time of a single exercise. @@ -24,6 +39,36 @@ namespace Fitness.Services long HoldTimeThreshold { get; set; } /// + /// Gets the property specifying the average score above the score threshold specified for the exercise. + /// + int AverageScore { get; } + + /// + /// Gets the property specifying the number of repetitions of the exercise. + /// + int Count { get; } + + /// + /// Gets the time remaining until the end of the exercise. + /// + TimeSpan TimeLeft { get; } + + /// + /// Starts workout. + /// + void StartWorkout(); + + /// + /// Stops workout. + /// + void StopWorkout(); + + /// + /// Resets workout. + /// + void ResetWorkout(); + + /// /// Detects performing the exercise based on given preview image data. /// /// Preview image data. diff --git a/Fitness/Services/Exercises/SquatService.cs b/Fitness/Services/Exercises/SquatService.cs index 084ebcc..5d296ef 100644 --- a/Fitness/Services/Exercises/SquatService.cs +++ b/Fitness/Services/Exercises/SquatService.cs @@ -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 allScores = new List(); /// - public event EventHandler ExerciseStateUpdated; + public event EventHandler ExerciseStateUpdated; + + /// + public int AverageScore + { + get => averageScore; + } + + /// + public int Count + { + get => count; + } /// public long HoldTimeThreshold @@ -40,6 +54,14 @@ namespace Fitness.Services } /// + public void ResetWorkout() + { + TimerConfigured = false; + count = 0; + allScores.Clear(); + } + + /// protected override void DetectExercise(Landmark[,] landmarks) { int numberOfBodyParts = landmarks.GetLength(1); @@ -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 index 0000000..dedba1b --- /dev/null +++ b/Fitness/Services/Exercises/TimeLeftChangedEventArgs.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Fitness.Services +{ + /// + /// The class that stores the exercise time left data. + /// + public class TimeLeftChangedEventArgs : EventArgs + { + /// + /// Gets or sets the time remaining until the end of the exercise. + /// + public TimeSpan TimeLeft { get; internal set; } + } +} diff --git a/Fitness/Services/WorkoutRepository.cs b/Fitness/Services/WorkoutRepository.cs index 4a2d98f..8551f92 100644 --- a/Fitness/Services/WorkoutRepository.cs +++ b/Fitness/Services/WorkoutRepository.cs @@ -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", diff --git a/Fitness/ViewModels/ExercisingViewModel.cs b/Fitness/ViewModels/ExercisingViewModel.cs index 6684f84..b1bd4d8 100644 --- a/Fitness/ViewModels/ExercisingViewModel.cs +++ b/Fitness/ViewModels/ExercisingViewModel.cs @@ -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; /// /// Initializes a new instance of the 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; } /// @@ -113,12 +121,50 @@ namespace Fitness.ViewModels /// /// Gets AverageScore. /// - public int AverageScore { get; private set; } = 98; + public int AverageScore + { + get => avarageScore; + private set + { + if (value != avarageScore) + { + avarageScore = value; + RaisePropertyChanged(); + } + } + } + + /// + /// Gets TotalTime. + /// + public TimeSpan TotalTime + { + get => totalTime; + private set + { + if (value != totalTime) + { + totalTime = value; + RaisePropertyChanged(); + } + } + } /// /// Gets TotalCount. /// - public int TotalCount { get; private set; } = 27; + public int TotalCount + { + get => totatCount; + private set + { + if (value != totatCount) + { + totatCount = value; + RaisePropertyChanged(); + } + } + } /// /// Gets the property specifying the correctness of the exercise - values ranging from 0 to 100. @@ -169,14 +215,20 @@ namespace Fitness.ViewModels } /// - /// Gets TotalTime. + /// Gets the time remaining until the end of the exercise. /// - public TimeSpan TotalTime { get; private set; } = new TimeSpan(0, 3, 39); - - /// - /// Gets the TimeLeft in workout. - /// - public TimeSpan TimeLeft { get; private set; } = TimeSpan.FromSeconds(27); + public TimeSpan TimeLeft + { + get => timeLeft; + private set + { + if (timeLeft != value) + { + timeLeft = value; + RaisePropertyChanged(); + } + } + } /// /// 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); + } } } diff --git a/Fitness/ViewModels/ScanningViewModel.cs b/Fitness/ViewModels/ScanningViewModel.cs index 690815d..089bf98 100644 --- a/Fitness/ViewModels/ScanningViewModel.cs +++ b/Fitness/ViewModels/ScanningViewModel.cs @@ -110,7 +110,7 @@ namespace Fitness.ViewModels /// 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; diff --git a/Fitness/Views/ExercisingView.xaml.cs b/Fitness/Views/ExercisingView.xaml.cs index a81eadc..bb55f45 100644 --- a/Fitness/Views/ExercisingView.xaml.cs +++ b/Fitness/Views/ExercisingView.xaml.cs @@ -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() -- 2.7.4