From dbd1e917b7f9840f3b11fda052624b79d89aafab Mon Sep 17 00:00:00 2001 From: Victor Kareh Date: Thu, 18 Dec 2025 16:33:18 -0500 Subject: [PATCH] hiit: Add new High Intensity Interval Training app Adds a configurable HIIT timer with active/rest intervals and sets. Has the following features: - Vibration feedback (3s before next active/rest period) - Pause with hardware button - Summary screen when workout ends --- src/CMakeLists.txt | 1 + src/displayapp/UserApps.h | 1 + src/displayapp/apps/Apps.h.in | 1 + src/displayapp/apps/CMakeLists.txt | 1 + src/displayapp/fonts/fonts.json | 4 +- src/displayapp/screens/Hiit.cpp | 440 +++++++++++++++++++++++++++++ src/displayapp/screens/Hiit.h | 117 ++++++++ src/displayapp/screens/Symbols.h | 1 + 8 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 src/displayapp/screens/Hiit.cpp create mode 100644 src/displayapp/screens/Hiit.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e4a354df64..cb8ff70e3a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -393,6 +393,7 @@ list(APPEND SOURCE_FILES displayapp/screens/Steps.cpp displayapp/screens/Timer.cpp displayapp/screens/Dice.cpp + displayapp/screens/Hiit.cpp displayapp/screens/PassKey.cpp displayapp/screens/Error.cpp displayapp/screens/Alarm.cpp diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 25926edc40..ea10917423 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -4,6 +4,7 @@ #include "displayapp/screens/Alarm.h" #include "displayapp/screens/Dice.h" +#include "displayapp/screens/Hiit.h" #include "displayapp/screens/Timer.h" #include "displayapp/screens/Twos.h" #include "displayapp/screens/Tile.h" diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index d440b598d1..f77a30d271 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -29,6 +29,7 @@ namespace Pinetime { Calculator, Steps, Dice, + Hiit, Weather, PassKey, QuickSettings, diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..04593a79f7 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -11,6 +11,7 @@ else () set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paddle") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Twos") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Hiit") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Calculator") diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 3221c2f171..6d34f4607a 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -7,7 +7,7 @@ }, { "file": "FontAwesome5-Solid+Brands+Regular.woff", - "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed" + "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed, 0xf44b" } ], "bpp": 1, @@ -18,7 +18,7 @@ "sources": [ { "file": "JetBrainsMono-Regular.ttf", - "range": "0x25, 0x2b, 0x2d, 0x2e, 0x30-0x3a, 0x43, 0x46, 0x4b-0x4d, 0x66, 0x69, 0x6b, 0x6d, 0x74, 0xb0" + "range": "0x25, 0x2b, 0x2d, 0x2e, 0x2f, 0x30-0x3a, 0x43, 0x46, 0x4b-0x4d, 0x66, 0x69, 0x6b, 0x6d, 0x74, 0xb0" } ], "bpp": 1, diff --git a/src/displayapp/screens/Hiit.cpp b/src/displayapp/screens/Hiit.cpp new file mode 100644 index 0000000000..6342ffee5d --- /dev/null +++ b/src/displayapp/screens/Hiit.cpp @@ -0,0 +1,440 @@ +#include "displayapp/screens/Hiit.h" +#include "displayapp/InfiniTimeTheme.h" + +using namespace Pinetime::Applications::Screens; + +Hiit::Hiit(Controllers::MotorController& motorController, Controllers::Settings& settingsController, System::SystemTask& systemTask) + : motorController {motorController}, settingsController {settingsController}, wakeLock(systemTask) { + + CreateSetupUI(); + + taskRefresh = lv_task_create(Screen::RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); +} + +Hiit::~Hiit() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); + + // Restore notification status if workout was in progress + if (currentState != State::Setup && currentState != State::Summary) { + settingsController.SetNotificationStatus(savedNotificationStatus); + } +} + +void Hiit::CreateSetupUI() { + lv_obj_clean(lv_scr_act()); + + // Active counter + activeCounter.Create(); + activeCounter.SetValue(activeDuration); + lv_obj_align(activeCounter.GetObject(), lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 6, -20); + + lv_obj_t* lblActive = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(lblActive, "ACTIVE"); + lv_obj_align(lblActive, activeCounter.GetObject(), LV_ALIGN_OUT_TOP_MID, 0, 0); + + // Rest counter + restCounter.Create(); + restCounter.SetValue(restDuration); + lv_obj_align(restCounter.GetObject(), lv_scr_act(), LV_ALIGN_CENTER, 0, -20); + + lv_obj_t* lblRest = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(lblRest, "REST"); + lv_obj_align(lblRest, restCounter.GetObject(), LV_ALIGN_OUT_TOP_MID, 0, 0); + + // Sets counter + setsCounter.Create(); + setsCounter.SetValue(totalSets); + lv_obj_align(setsCounter.GetObject(), lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -6, -20); + + lv_obj_t* lblSets = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(lblSets, "SETS"); + lv_obj_align(lblSets, setsCounter.GetObject(), LV_ALIGN_OUT_TOP_MID, 0, 0); + + // Start button + btnStart = lv_btn_create(lv_scr_act(), nullptr); + btnStart->user_data = this; + lv_obj_set_event_cb(btnStart, OnStartClicked); + lv_obj_set_size(btnStart, 120, 50); + lv_obj_align(btnStart, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, -10); + lv_obj_set_style_local_bg_color(btnStart, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::green); + + lblStart = lv_label_create(btnStart, nullptr); + lv_label_set_text_static(lblStart, Symbols::play); + lv_obj_set_style_local_text_font(lblStart, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); +} + +void Hiit::CreateWorkoutUI() { + lv_obj_clean(lv_scr_act()); + + // Set info + lblSetInfo = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblSetInfo, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_obj_align(lblSetInfo, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 10, 10); + + // Phase label (ACTIVE/REST) + lblPhase = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblPhase, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_obj_align(lblPhase, lv_scr_act(), LV_ALIGN_CENTER, 0, -50); + lv_obj_set_auto_realign(lblPhase, true); + + // Countdown timer + lblCountdown = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblCountdown, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + lv_obj_align(lblCountdown, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); + lv_obj_set_auto_realign(lblCountdown, true); + + // Total elapsed time + lblTotalTime = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblTotalTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_label_set_align(lblTotalTime, LV_LABEL_ALIGN_CENTER); + lv_obj_align(lblTotalTime, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, -10); + lv_obj_set_auto_realign(lblTotalTime, true); +} + +void Hiit::CreatePausedUI() { + lv_obj_clean(lv_scr_act()); + + // Set info + lblSetInfo = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblSetInfo, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_label_set_text_fmt(lblSetInfo, "Set %d/%d", currentSet, totalSets); + lv_obj_align(lblSetInfo, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 10, 10); + + // Paused label + lv_obj_t* lblPaused = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(lblPaused, "PAUSED"); + lv_obj_set_style_local_text_font(lblPaused, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_obj_set_style_local_text_color(lblPaused, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); + lv_obj_align(lblPaused, lv_scr_act(), LV_ALIGN_CENTER, 0, -50); + + // Frozen countdown + lblCountdown = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblCountdown, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + uint8_t remaining = GetRemainingSeconds(); + lv_label_set_text_fmt(lblCountdown, "%d:%02d", remaining / 60, remaining % 60); + lv_obj_align(lblCountdown, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); + + // Resume button + btnResume = lv_btn_create(lv_scr_act(), nullptr); + btnResume->user_data = this; + lv_obj_set_event_cb(btnResume, OnResumeClicked); + lv_obj_set_size(btnResume, 140, 50); + lv_obj_align(btnResume, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, -10); + lv_obj_set_style_local_bg_color(btnResume, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::blue); + + lblResume = lv_label_create(btnResume, nullptr); + lv_label_set_text_static(lblResume, "RESUME"); + lv_obj_set_style_local_text_font(lblResume, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); +} + +void Hiit::CreateSummaryUI(uint32_t finalTime) { + lv_obj_clean(lv_scr_act()); + + bool completed = (currentSet >= totalSets); + + // Status label + lblStatus = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblStatus, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + + // Sets completed + lblSetsCompleted = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(lblSetsCompleted, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); + + if (completed) { + lv_label_set_text_static(lblStatus, "COMPLETE!"); + lv_obj_set_style_local_text_color(lblStatus, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::green); + lv_label_set_text_fmt(lblSetsCompleted, "%d", totalSets); + } else { + lv_label_set_text_static(lblStatus, "STOPPED"); + lv_obj_set_style_local_text_color(lblStatus, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); + lv_label_set_text_fmt(lblSetsCompleted, "%d/%d", currentSet, totalSets); + } + lv_obj_align(lblStatus, lv_scr_act(), LV_ALIGN_IN_TOP_MID, 0, 0); + lv_obj_align(lblSetsCompleted, lv_scr_act(), LV_ALIGN_CENTER, 0, -48); + + lv_obj_t* lblSetsLabel = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(lblSetsLabel, "Sets"); + lv_obj_set_style_local_text_font(lblSetsLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_obj_align(lblSetsLabel, lblSetsCompleted, LV_ALIGN_OUT_TOP_MID, 0, 0); + + // Total time + char timeBuffer[16]; + FormatTime(timeBuffer, finalTime); + + lblFinalTime = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(lblFinalTime, timeBuffer); + lv_obj_set_style_local_text_font(lblFinalTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); + lv_obj_align(lblFinalTime, lv_scr_act(), LV_ALIGN_CENTER, 0, 24); + + lv_obj_t* lblTimeLabel = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(lblTimeLabel, "Total Time"); + lv_obj_set_style_local_text_font(lblTimeLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); + lv_obj_align(lblTimeLabel, lblFinalTime, LV_ALIGN_OUT_TOP_MID, 0, 0); + + // Done button + btnDone = lv_btn_create(lv_scr_act(), nullptr); + btnDone->user_data = this; + lv_obj_set_event_cb(btnDone, OnDoneClicked); + lv_obj_set_size(btnDone, 120, 50); + lv_obj_align(btnDone, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, -10); + lv_obj_set_style_local_bg_color(btnDone, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::blue); + + lblDone = lv_label_create(btnDone, nullptr); + lv_label_set_text_static(lblDone, "DONE"); + lv_obj_set_style_local_text_font(lblDone, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); +} + +void Hiit::StartWorkout() { + // Get config values from counters + activeDuration = activeCounter.GetValue(); + restDuration = restCounter.GetValue(); + totalSets = setsCounter.GetValue(); + + // Save and disable notifications during workout + savedNotificationStatus = settingsController.GetNotificationStatus(); + settingsController.SetNotificationStatus(Controllers::Settings::Notification::Off); + + // Initialize state + currentState = State::Starting; + currentSet = 0; + phaseStartTime = xTaskGetTickCount(); + workoutStartTime = 0; // Will be set after initial countdown + + wakeLock.Lock(); + + CreateWorkoutUI(); +} + +void Hiit::TransitionToActive() { + currentSet++; + currentState = State::Active; + phaseStartTime = xTaskGetTickCount(); + lastCountdownValue = 0; +} + +void Hiit::TransitionToRest() { + currentState = State::Rest; + phaseStartTime = xTaskGetTickCount(); + lastCountdownValue = 0; +} + +void Hiit::Pause() { + stateBeforePause = currentState; + currentState = State::Paused; + pauseStartTime = xTaskGetTickCount(); + + CreatePausedUI(); +} + +void Hiit::Resume() { + // Calculate how long we were paused + TickType_t pauseDuration = xTaskGetTickCount() - pauseStartTime; + + // Adjust start times to account for pause + phaseStartTime += pauseDuration; + workoutStartTime += pauseDuration; + + currentState = stateBeforePause; + + CreateWorkoutUI(); + UpdateWorkoutDisplay(); +} + +void Hiit::EndWorkout() { + // Calculate elapsed time before changing state + uint32_t finalTime = GetElapsedWorkoutTime(); + + // Restore notification status + settingsController.SetNotificationStatus(savedNotificationStatus); + + currentState = State::Summary; + + CreateSummaryUI(finalTime); +} + +void Hiit::UpdateWorkoutDisplay() { + // Update set info + if (lblSetInfo != nullptr) { + lv_label_set_text_fmt(lblSetInfo, "Set %d/%d", currentSet, totalSets); + } + + // Update phase label + if (lblPhase != nullptr) { + if (currentState == State::Active) { + lv_label_set_text_static(lblPhase, "ACTIVE"); + lv_obj_set_style_local_text_color(lblPhase, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::green); + } else if (currentState == State::Rest) { + lv_label_set_text_static(lblPhase, "REST"); + lv_obj_set_style_local_text_color(lblPhase, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); + } + } + + // Update countdown + if (lblCountdown != nullptr) { + uint8_t remaining = GetRemainingSeconds(); + lv_label_set_text_fmt(lblCountdown, "%d:%02d", remaining / 60, remaining % 60); + } + + // Update total time + if (lblTotalTime != nullptr) { + uint32_t totalSeconds = GetElapsedWorkoutTime(); + char timeBuffer[16]; + FormatTime(timeBuffer, totalSeconds); + lv_label_set_text_fmt(lblTotalTime, "Total: %s", timeBuffer); + } +} + +void Hiit::Refresh() { + switch (currentState) { + case State::Setup: + case State::Summary: + // Nothing to update + break; + + case State::Paused: + // Frozen, nothing to update + break; + + case State::Starting: { + // Do a 3 second countdown before starting workout + TickType_t elapsed = xTaskGetTickCount() - phaseStartTime; + uint32_t elapsedSeconds = elapsed / configTICK_RATE_HZ; + uint8_t remaining = (elapsedSeconds >= 3) ? 0 : (3 - elapsedSeconds); + + if (lblSetInfo != nullptr) { + lv_label_set_text_static(lblSetInfo, ""); + } + if (lblPhase != nullptr) { + lv_label_set_text_static(lblPhase, "GET READY"); + lv_obj_set_style_local_text_color(lblPhase, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); + } + if (lblCountdown != nullptr) { + lv_label_set_text_fmt(lblCountdown, "%d", remaining); + } + if (lblTotalTime != nullptr) { + lv_label_set_text_static(lblTotalTime, ""); + } + + // Countdown pulse + if (remaining <= 3 && remaining > 0 && remaining != lastCountdownValue) { + lastCountdownValue = remaining; + motorController.RunForDuration(50); + } + + // Start workout after countdown + if (remaining == 0) { + workoutStartTime = xTaskGetTickCount(); + motorController.RunForDuration(100); + TransitionToActive(); + } + break; + } + + case State::Active: + case State::Rest: { + UpdateWorkoutDisplay(); + + uint8_t remaining = GetRemainingSeconds(); + + // Countdown pulse for last 3 seconds + if (remaining <= 3 && remaining > 0 && remaining != lastCountdownValue) { + lastCountdownValue = remaining; + motorController.RunForDuration(50); + } + + // Check for phase transition + if (remaining == 0) { + motorController.RunForDuration(100); + if (currentState == State::Active) { + // Check if this was the final active interval + if (currentSet >= totalSets) { + EndWorkout(); + } else { + TransitionToRest(); + } + } else { + // End of rest period - always transition to next active + TransitionToActive(); + } + } + break; + } + } +} + +bool Hiit::OnButtonPushed() { + if (currentState == State::Starting) { + // Cancel initial countdown + settingsController.SetNotificationStatus(savedNotificationStatus); + wakeLock.Release(); + currentState = State::Setup; + currentSet = 0; + CreateSetupUI(); + return true; + } else if (currentState == State::Active || currentState == State::Rest) { + Pause(); + return true; + } else if (currentState == State::Paused) { + // Stop workout and show summary + EndWorkout(); + return true; + } + return false; +} + +bool Hiit::OnTouchEvent(TouchEvents event) { + // Prevent closing the app during active workout phases + return (currentState == State::Active || currentState == State::Rest) && event == TouchEvents::SwipeDown; +} + +uint8_t Hiit::GetRemainingSeconds() const { + TickType_t elapsed = xTaskGetTickCount() - phaseStartTime; + bool isActivePhase = (currentState == State::Active || (currentState == State::Paused && stateBeforePause == State::Active)); + uint8_t phaseDuration = isActivePhase ? activeDuration : restDuration; + uint32_t elapsedSeconds = elapsed / configTICK_RATE_HZ; + + if (elapsedSeconds >= phaseDuration) { + return 0; + } + return phaseDuration - elapsedSeconds; +} + +uint32_t Hiit::GetElapsedWorkoutTime() const { + TickType_t now = xTaskGetTickCount(); + if (currentState == State::Paused) { + now = pauseStartTime; // Stop counting at pause time + } + TickType_t elapsed = now - workoutStartTime; + return elapsed / configTICK_RATE_HZ; +} + +void Hiit::FormatTime(char* buffer, uint32_t seconds) const { + uint32_t minutes = seconds / 60; + uint32_t secs = seconds % 60; + sprintf(buffer, "%02lu:%02lu", minutes, secs); +} + +void Hiit::OnStartClicked(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + auto* screen = static_cast(obj->user_data); + screen->StartWorkout(); + } +} + +void Hiit::OnResumeClicked(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + auto* screen = static_cast(obj->user_data); + screen->Resume(); + } +} + +void Hiit::OnDoneClicked(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + auto* screen = static_cast(obj->user_data); + screen->wakeLock.Release(); + screen->currentState = State::Setup; + screen->currentSet = 0; + screen->CreateSetupUI(); + } +} diff --git a/src/displayapp/screens/Hiit.h b/src/displayapp/screens/Hiit.h new file mode 100644 index 0000000000..8c257b3c97 --- /dev/null +++ b/src/displayapp/screens/Hiit.h @@ -0,0 +1,117 @@ +#pragma once + +#include "displayapp/screens/Screen.h" +#include "components/motor/MotorController.h" +#include "components/settings/Settings.h" +#include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" +#include "displayapp/LittleVgl.h" +#include "displayapp/widgets/Counter.h" +#include +#include "Symbols.h" + +namespace Pinetime::Applications { + namespace Screens { + class Hiit : public Screen { + public: + Hiit(Controllers::MotorController& motorController, Controllers::Settings& settingsController, System::SystemTask& systemTask); + ~Hiit() override; + void Refresh() override; + bool OnButtonPushed() override; + bool OnTouchEvent(TouchEvents event) override; + + private: + enum class State { Setup, Starting, Active, Rest, Paused, Summary }; + + // State management + State currentState = State::Setup; + State stateBeforePause = State::Active; + + // Configuration (in seconds) + uint8_t activeDuration = 40; + uint8_t restDuration = 20; + uint8_t totalSets = 8; + + // Runtime state + uint8_t currentSet = 0; + TickType_t phaseStartTime = 0; + TickType_t workoutStartTime = 0; + TickType_t pauseStartTime = 0; + uint8_t lastCountdownValue = 0; + + // Controllers + Controllers::MotorController& motorController; + Controllers::Settings& settingsController; + System::WakeLock wakeLock; + Controllers::Settings::Notification savedNotificationStatus = Controllers::Settings::Notification::On; + + // Setup UI elements + Widgets::Counter activeCounter = Widgets::Counter(1, 99, jetbrains_mono_42); + Widgets::Counter restCounter = Widgets::Counter(1, 99, jetbrains_mono_42); + Widgets::Counter setsCounter = Widgets::Counter(1, 99, jetbrains_mono_42); + lv_obj_t* btnStart = nullptr; + lv_obj_t* lblStart = nullptr; + + // Workout UI elements + lv_obj_t* lblSetInfo = nullptr; + lv_obj_t* lblPhase = nullptr; + lv_obj_t* lblCountdown = nullptr; + lv_obj_t* lblTotalTime = nullptr; + + // Paused UI elements + lv_obj_t* btnResume = nullptr; + lv_obj_t* lblResume = nullptr; + + // Summary UI elements + lv_obj_t* lblStatus = nullptr; + lv_obj_t* lblSetsCompleted = nullptr; + lv_obj_t* lblFinalTime = nullptr; + lv_obj_t* btnDone = nullptr; + lv_obj_t* lblDone = nullptr; + + // Task for refresh + lv_task_t* taskRefresh = nullptr; + + // UI creation methods + void CreateSetupUI(); + void CreateWorkoutUI(); + void CreatePausedUI(); + void CreateSummaryUI(uint32_t finalTime); + + // Transition methods + void StartWorkout(); + void TransitionToActive(); + void TransitionToRest(); + void Pause(); + void Resume(); + void EndWorkout(); + + // Update methods + void UpdateWorkoutDisplay(); + + // Utility + uint8_t GetRemainingSeconds() const; + uint32_t GetElapsedWorkoutTime() const; + void FormatTime(char* buffer, uint32_t seconds) const; + + // Event handlers + static void OnStartClicked(lv_obj_t* obj, lv_event_t event); + static void OnResumeClicked(lv_obj_t* obj, lv_event_t event); + static void OnDoneClicked(lv_obj_t* obj, lv_event_t event); + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Hiit; + static constexpr const char* icon = Screens::Symbols::dumbbell; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Hiit(controllers.motorController, controllers.settingsController, *controllers.systemTask); + } + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; +} diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index 058b2d06e9..67d1c7f12e 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -37,6 +37,7 @@ namespace Pinetime { static constexpr const char* lapsFlag = "\xEF\x80\xA4"; static constexpr const char* drum = "\xEF\x95\xA9"; static constexpr const char* dice = "\xEF\x94\xA2"; + static constexpr const char* dumbbell = "\xEF\x91\x8B"; static constexpr const char* eye = "\xEF\x81\xAE"; static constexpr const char* home = "\xEF\x80\x95"; static constexpr const char* sleep = "\xEE\xBD\x84";