3a938236d4
The lv_obj_del is called on btnStopLap when transitioning to the initial state, however the variable isn't then set to null. A subsequent call to Refresh would attempt to delete the already freed object. This could be triggered by stopping the stop watch, then pressing the physical button on the watch. Fixes https://github.com/JF002/InfiniTime/issues/315
228 lines
8.1 KiB
C++
228 lines
8.1 KiB
C++
#include "StopWatch.h"
|
|
|
|
#include "Screen.h"
|
|
#include "Symbols.h"
|
|
#include "lvgl/lvgl.h"
|
|
#include "projdefs.h"
|
|
#include "FreeRTOSConfig.h"
|
|
#include "task.h"
|
|
|
|
#include <tuple>
|
|
|
|
using namespace Pinetime::Applications::Screens;
|
|
|
|
// Anonymous namespace for local functions
|
|
namespace {
|
|
TimeSeparated_t convertTicksToTimeSegments(const TickType_t timeElapsed) {
|
|
const int timeElapsedMillis = (static_cast<float>(timeElapsed) / static_cast<float>(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<StopWatch*>(obj->user_data);
|
|
stopWatch->playPauseBtnEventHandler(event);
|
|
}
|
|
|
|
static void stop_lap_event_handler(lv_obj_t* obj, lv_event_t event) {
|
|
auto stopWatch = static_cast<StopWatch*>(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.
|
|
}
|
|
}
|
|
}
|