InfiniTime/src/displayapp/screens/StopWatch.cpp
Martin Ashby 3a938236d4
Fix a possible double free in StopWatch::Refresh.
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
2021-05-03 08:11:58 +01:00

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.
}
}
}