#include "StopWatch.h" #include "Screen.h" #include "Symbols.h" #include "lvgl/lvgl.h" #include "projdefs.h" #include "FreeRTOSConfig.h" #include "task.h" #include using namespace Pinetime::Applications::Screens; // Anonymous namespace for local functions namespace { TimeSeparated_t convertTicksToTimeSegments(const TickType_t timeElapsed) { const int timeElapsedMillis = (static_cast(timeElapsed) / static_cast(configTICK_RATE_HZ)) * 1000; const int hundredths = (timeElapsedMillis % 1000) / 10; // Get only the first two digits and ignore the last const int secs = (timeElapsedMillis / 1000) % 60; const int mins = (timeElapsedMillis / 1000) / 60; return TimeSeparated_t {mins, secs, hundredths}; } TickType_t calculateDelta(const TickType_t startTime, const TickType_t currentTime) { TickType_t delta = 0; // Take care of overflow if (startTime > currentTime) { delta = 0xffffffff - startTime; delta += (currentTime + 1); } else { delta = currentTime - startTime; } return delta; } } static void play_pause_event_handler(lv_obj_t* obj, lv_event_t event) { auto stopWatch = static_cast(obj->user_data); stopWatch->playPauseBtnEventHandler(event); } static void stop_lap_event_handler(lv_obj_t* obj, lv_event_t event) { auto stopWatch = static_cast(obj->user_data); stopWatch->stopLapBtnEventHandler(event); } StopWatch::StopWatch(DisplayApp* app) : Screen(app), running {true}, currentState {States::Init}, currentEvent {Events::Stop}, startTime {}, oldTimeElapsed {}, currentTimeSeparated {}, lapBuffer {}, lapNr {}, lapPressed {false} { time = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_font(time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); lv_obj_set_style_local_text_color(time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); lv_label_set_text(time, "00:00"); lv_obj_align(time, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, -45); msecTime = lv_label_create(lv_scr_act(), nullptr); // lv_obj_set_style_local_text_font(msecTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); lv_obj_set_style_local_text_color(msecTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); lv_label_set_text(msecTime, "00"); lv_obj_align(msecTime, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 108, 3); btnPlayPause = lv_btn_create(lv_scr_act(), nullptr); btnPlayPause->user_data = this; lv_obj_set_event_cb(btnPlayPause, play_pause_event_handler); lv_obj_align(btnPlayPause, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, -10); lv_obj_set_height(btnPlayPause, 40); txtPlayPause = lv_label_create(btnPlayPause, nullptr); lv_label_set_text(txtPlayPause, Symbols::play); lapOneText = lv_label_create(lv_scr_act(), nullptr); // lv_obj_set_style_local_text_font(lapOneText, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); lv_obj_set_style_local_text_color(lapOneText, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_YELLOW); lv_obj_align(lapOneText, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 50, 30); lv_label_set_text(lapOneText, ""); lapTwoText = lv_label_create(lv_scr_act(), nullptr); // lv_obj_set_style_local_text_font(lapTwoText, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20); lv_obj_set_style_local_text_color(lapTwoText, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_YELLOW); lv_obj_align(lapTwoText, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 50, 55); lv_label_set_text(lapTwoText, ""); // We don't want this button in the init state btnStopLap = nullptr; } StopWatch::~StopWatch() { lv_obj_clean(lv_scr_act()); } bool StopWatch::Refresh() { // @startuml CHIP8_state // State "Init" as init // State "Running" as run // State "Halted" as halt // [*] --> init // init -> run : press play // run -> run : press lap // run --> halt : press pause // halt --> run : press play // halt --> init : press stop // @enduml // Copy paste the above plantuml text to visualize the state diagram switch (currentState) { // Init state when an user first opens the app // and when a stop/reset button is pressed case States::Init: { if (btnStopLap != nullptr) { lv_obj_del(btnStopLap); btnStopLap = nullptr; } // The initial default value lv_label_set_text(time, "00:00"); lv_label_set_text(msecTime, "00"); lv_label_set_text(lapOneText, ""); lv_label_set_text(lapTwoText, ""); lapBuffer.clearBuffer(); lapNr = 0; if (currentEvent == Events::Play) { btnStopLap = lv_btn_create(lv_scr_act(), nullptr); btnStopLap->user_data = this; lv_obj_set_event_cb(btnStopLap, stop_lap_event_handler); lv_obj_align(btnStopLap, lv_scr_act(), LV_ALIGN_IN_TOP_MID, 0, 0); lv_obj_set_height(btnStopLap, 40); txtStopLap = lv_label_create(btnStopLap, nullptr); lv_label_set_text(txtStopLap, Symbols::lapsFlag); startTime = xTaskGetTickCount(); currentState = States::Running; } break; } case States::Running: { lv_label_set_text(txtPlayPause, Symbols::pause); lv_label_set_text(txtStopLap, Symbols::lapsFlag); const auto timeElapsed = calculateDelta(startTime, xTaskGetTickCount()); currentTimeSeparated = convertTicksToTimeSegments((oldTimeElapsed + timeElapsed)); lv_label_set_text_fmt(time, "%02d:%02d", currentTimeSeparated.mins, currentTimeSeparated.secs); lv_label_set_text_fmt(msecTime, "%02d", currentTimeSeparated.hundredths); if (lapPressed == true) { if (lapBuffer[1]) { lv_label_set_text_fmt( lapOneText, "#%2d %2d:%02d.%02d", (lapNr - 1), lapBuffer[1]->mins, lapBuffer[1]->secs, lapBuffer[1]->hundredths); } if (lapBuffer[0]) { lv_label_set_text_fmt( lapTwoText, "#%2d %2d:%02d.%02d", lapNr, lapBuffer[0]->mins, lapBuffer[0]->secs, lapBuffer[0]->hundredths); } // Reset the bool to avoid setting the text in each cycle until there is a change lapPressed = false; } if (currentEvent == Events::Pause) { // Reset the start time startTime = 0; // Store the current time elapsed in cache oldTimeElapsed += timeElapsed; currentState = States::Halted; lv_obj_set_style_local_text_color(time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_YELLOW); lv_obj_set_style_local_text_color(msecTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_YELLOW); } else { lv_obj_set_style_local_text_color(time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); lv_obj_set_style_local_text_color(msecTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); } break; } case States::Halted: { lv_label_set_text(txtPlayPause, Symbols::play); lv_label_set_text(txtStopLap, Symbols::stop); if (currentEvent == Events::Play) { startTime = xTaskGetTickCount(); currentState = States::Running; } if (currentEvent == Events::Stop) { currentState = States::Init; oldTimeElapsed = 0; lv_obj_set_style_local_text_color(time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); lv_obj_set_style_local_text_color(msecTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); } break; } } return running; } void StopWatch::playPauseBtnEventHandler(lv_event_t event) { if (event == LV_EVENT_CLICKED) { if (currentState == States::Init) { currentEvent = Events::Play; } else { // Simple Toggle for play/pause currentEvent = (currentEvent == Events::Play ? Events::Pause : Events::Play); } } } void StopWatch::stopLapBtnEventHandler(lv_event_t event) { if (event == LV_EVENT_CLICKED) { // If running, then this button is used to save laps if (currentState == States::Running) { lapBuffer.addLaps(currentTimeSeparated); lapNr++; lapPressed = true; } else if (currentState == States::Halted) { currentEvent = Events::Stop; } else { // Not possible to reach here. Do nothing. } } }