diff --git a/README.md b/README.md index c277b36c..96e35d8f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ As of now, here is the list of achievements of this project: * Notification (displays the last notification received) * Paddle (single player pong-like game) * Two (2048 clone game) + * Stopwatch (with all the necessary functions such as play, pause, lap, stop) - Supported by 2 companion apps (development is in progress): * [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) (on Android) * [Amazfish](https://openrepos.net/content/piggz/amazfish) (on SailfishOS and Linux) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ac533540..59dc9730 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -485,6 +485,7 @@ list(APPEND SOURCE_FILES displayapp/screens/Meter.cpp displayapp/screens/InfiniPaint.cpp displayapp/screens/Paddle.cpp + displayapp/screens/StopWatch.cpp displayapp/screens/BatteryIcon.cpp displayapp/screens/BleIcon.cpp displayapp/screens/NotificationIcon.cpp @@ -644,6 +645,7 @@ set(INCLUDE_FILES displayapp/screens/Tile.h displayapp/screens/Meter.h displayapp/screens/InfiniPaint.h + displayapp/screens/StopWatch.h displayapp/screens/Paddle.h displayapp/screens/DropDownDemo.h displayapp/screens/BatteryIcon.h diff --git a/src/displayapp/Apps.h b/src/displayapp/Apps.h index 028fc80c..74b121df 100644 --- a/src/displayapp/Apps.h +++ b/src/displayapp/Apps.h @@ -2,6 +2,6 @@ namespace Pinetime { namespace Applications { - enum class Apps {None, Launcher, Clock, SysInfo, Meter, Brightness, Music, FirmwareValidation, Paint, Paddle, Notifications, Twos, HeartRate, Navigation}; + enum class Apps {None, Launcher, Clock, SysInfo, Meter, Brightness, Music, FirmwareValidation, Paint, Paddle, Notifications, Twos, HeartRate, Navigation, StopWatch}; } } diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index fee973ac..2e81c99b 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -12,6 +12,7 @@ #include "displayapp/screens/FirmwareValidation.h" #include "displayapp/screens/InfiniPaint.h" #include "displayapp/screens/Paddle.h" +#include "displayapp/screens/StopWatch.h" #include "displayapp/screens/Meter.h" #include "displayapp/screens/Music.h" #include "displayapp/screens/Navigation.h" @@ -204,6 +205,7 @@ void DisplayApp::RunningState() { break; case Apps::SysInfo: currentScreen.reset(new Screens::SystemInfo(this, dateTimeController, batteryController, brightnessController, bleController, watchdog)); break; case Apps::Meter: currentScreen.reset(new Screens::Meter(this)); break; + case Apps::StopWatch: currentScreen.reset(new Screens::StopWatch(this)); break; case Apps::Twos: currentScreen.reset(new Screens::Twos(this)); break; case Apps::Paint: currentScreen.reset(new Screens::InfiniPaint(this, lvgl)); break; case Apps::Paddle: currentScreen.reset(new Screens::Paddle(this, lvgl)); break; diff --git a/src/displayapp/fonts/Readme.md b/src/displayapp/fonts/Readme.md index 8e50c297..79b36bca 100644 --- a/src/displayapp/fonts/Readme.md +++ b/src/displayapp/fonts/Readme.md @@ -10,7 +10,7 @@ * Bpp : 1 bit-per-pixel * Do not enable font compression and horizontal subpixel hinting * Load the file `JetBrainsMono-Bold.tff` and specify the following range : `0x20-0x7f, 0x410-0x44f` - * Add a 2nd font, load the file `FontAwesome5-Solid+Brands+Regular.woff` and specify the following range : `0xf293, 0xf294, 0xf244, 0xf240, 0xf242, 0xf243, 0xf241, 0xf54b, 0xf21e, 0xf1e6, 0xf54b, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf069, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf029, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd` + * Add a 2nd font, load the file `FontAwesome5-Solid+Brands+Regular.woff` and specify the following range : `0xf293, 0xf294, 0xf244, 0xf240, 0xf242, 0xf243, 0xf241, 0xf54b, 0xf21e, 0xf1e6, 0xf54b, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf069, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf029, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024` * Click on Convert, and download the file `jetbrains_mono_bold_20.c` and copy it in `src/DisplayApp/Fonts` Add new symbols: diff --git a/src/displayapp/fonts/jetbrains_mono_bold_20.c b/src/displayapp/fonts/jetbrains_mono_bold_20.c index dc30104a..f4050db8 100644 --- a/src/displayapp/fonts/jetbrains_mono_bold_20.c +++ b/src/displayapp/fonts/jetbrains_mono_bold_20.c @@ -466,8 +466,8 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t gylph_bitmap[] = { 0x66, 0x66, 0x66, 0x6c, 0x63, /* U+417 "З" */ - 0x1f, 0x8f, 0xfd, 0xc7, 0x80, 0x70, 0x1c, 0x3e, - 0x7, 0xf0, 0xf, 0x0, 0xe0, 0x1d, 0x83, 0xb8, + 0x1f, 0xf, 0xf3, 0xc7, 0x0, 0x60, 0x1c, 0x1e, + 0x3, 0xf0, 0xe, 0x0, 0xe0, 0x1f, 0x83, 0xf8, 0xf7, 0xfc, 0x3e, 0x0, /* U+418 "И" */ @@ -615,7 +615,7 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t gylph_bitmap[] = { 0x70, /* U+437 "з" */ - 0x3f, 0x1f, 0xfe, 0x1c, 0x7, 0x1f, 0x7, 0xe0, + 0x3f, 0x1f, 0xfe, 0x1c, 0x7, 0x1f, 0x87, 0xe0, 0x1c, 0x7, 0xe1, 0xdf, 0xe3, 0xf0, /* U+438 "и" */ @@ -738,6 +738,15 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t gylph_bitmap[] = { 0xcf, 0x9f, 0xff, 0xf1, 0xff, 0xfc, 0x1f, 0xff, 0x1, 0xff, 0xc0, 0x1f, 0xf0, 0x0, 0x70, 0x0, + /* U+F024 "" */ + 0x70, 0x0, 0xf, 0x80, 0x0, 0xf8, 0x0, 0xf, + 0xff, 0xf, 0x7f, 0xff, 0xf7, 0xff, 0xff, 0x7f, + 0xff, 0xf7, 0xff, 0xff, 0x7f, 0xff, 0xf7, 0xff, + 0xff, 0x7f, 0xff, 0xf7, 0xff, 0xff, 0x7f, 0xff, + 0xf7, 0xff, 0xff, 0x7f, 0x7f, 0xe7, 0x0, 0x78, + 0x70, 0x0, 0x7, 0x0, 0x0, 0x70, 0x0, 0x7, + 0x0, 0x0, + /* U+F027 "" */ 0x0, 0xc0, 0x3, 0x80, 0xf, 0x0, 0x3e, 0xf, 0xfc, 0x9f, 0xf9, 0xbf, 0xf1, 0xff, 0xe3, 0xff, @@ -789,6 +798,13 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t gylph_bitmap[] = { 0xff, 0xfc, 0xff, 0xff, 0x3f, 0xff, 0xcf, 0xff, 0xf3, 0xff, 0xfc, 0xff, 0x7e, 0x1f, 0x80, + /* U+F04D "" */ + 0x7f, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0x80, + /* U+F051 "" */ 0xe0, 0x3f, 0x81, 0xfe, 0xf, 0xf8, 0x7f, 0xe3, 0xff, 0x9f, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, @@ -897,6 +913,13 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t gylph_bitmap[] = { 0x81, 0xf8, 0x6d, 0x99, 0x9a, 0x36, 0x7, 0x80, 0xe0, 0x18, 0x2, 0x0, 0x0, + /* U+F2F2 "" */ + 0x7, 0xe0, 0x7, 0xe0, 0x1, 0x80, 0x3, 0xc0, + 0xf, 0xf2, 0x1f, 0xff, 0x3e, 0x7e, 0x7e, 0x7e, + 0xfe, 0x7e, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, + 0xfe, 0x7f, 0xff, 0xff, 0x7f, 0xfe, 0x7f, 0xfe, + 0x3f, 0xfc, 0x1f, 0xf8, 0x7, 0xe0, + /* U+F3DD "" */ 0x40, 0x0, 0x40, 0x70, 0x0, 0x7e, 0x3c, 0x0, 0x3f, 0x8f, 0x80, 0x1f, 0x81, 0xe0, 0x1f, 0xc0, @@ -1080,7 +1103,7 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1514, .adv_w = 192, .box_w = 11, .box_h = 17, .ofs_x = 1, .ofs_y = -3}, {.bitmap_index = 1538, .adv_w = 192, .box_w = 9, .box_h = 14, .ofs_x = 2, .ofs_y = 0}, {.bitmap_index = 1554, .adv_w = 192, .box_w = 12, .box_h = 14, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1575, .adv_w = 192, .box_w = 11, .box_h = 14, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 1575, .adv_w = 192, .box_w = 11, .box_h = 14, .ofs_x = 1, .ofs_y = 0}, {.bitmap_index = 1595, .adv_w = 192, .box_w = 9, .box_h = 14, .ofs_x = 1, .ofs_y = 0}, {.bitmap_index = 1611, .adv_w = 192, .box_w = 9, .box_h = 19, .ofs_x = 1, .ofs_y = 0}, {.bitmap_index = 1633, .adv_w = 192, .box_w = 10, .box_h = 14, .ofs_x = 2, .ofs_y = 0}, @@ -1139,36 +1162,39 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2498, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = 0}, {.bitmap_index = 2511, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = -3}, {.bitmap_index = 2561, .adv_w = 320, .box_w = 19, .box_h = 20, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 2609, .adv_w = 240, .box_w = 15, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 2638, .adv_w = 360, .box_w = 23, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2693, .adv_w = 280, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 2732, .adv_w = 320, .box_w = 20, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 2775, .adv_w = 280, .box_w = 13, .box_h = 17, .ofs_x = 2, .ofs_y = -1}, - {.bitmap_index = 2803, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 2851, .adv_w = 280, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 2890, .adv_w = 280, .box_w = 13, .box_h = 17, .ofs_x = 2, .ofs_y = -1}, - {.bitmap_index = 2918, .adv_w = 320, .box_w = 19, .box_h = 20, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 2966, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 3019, .adv_w = 120, .box_w = 8, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3038, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 3088, .adv_w = 240, .box_w = 15, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3124, .adv_w = 320, .box_w = 20, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3172, .adv_w = 320, .box_w = 20, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 3215, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 3253, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 3291, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 3329, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 3367, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 3405, .adv_w = 280, .box_w = 15, .box_h = 20, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 3443, .adv_w = 200, .box_w = 11, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 3472, .adv_w = 400, .box_w = 25, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 3538, .adv_w = 360, .box_w = 23, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 3587, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3637, .adv_w = 400, .box_w = 25, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3697, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 3750, .adv_w = 360, .box_w = 22, .box_h = 20, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3805, .adv_w = 360, .box_w = 22, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3858, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0} + {.bitmap_index = 2609, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 2659, .adv_w = 240, .box_w = 15, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 2688, .adv_w = 360, .box_w = 23, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2743, .adv_w = 280, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 2782, .adv_w = 320, .box_w = 20, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 2825, .adv_w = 280, .box_w = 13, .box_h = 17, .ofs_x = 2, .ofs_y = -1}, + {.bitmap_index = 2853, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 2901, .adv_w = 280, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 2940, .adv_w = 280, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 2979, .adv_w = 280, .box_w = 13, .box_h = 17, .ofs_x = 2, .ofs_y = -1}, + {.bitmap_index = 3007, .adv_w = 320, .box_w = 19, .box_h = 20, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 3055, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 3108, .adv_w = 120, .box_w = 8, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3127, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 3177, .adv_w = 240, .box_w = 15, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3213, .adv_w = 320, .box_w = 20, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3261, .adv_w = 320, .box_w = 20, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 3304, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 3342, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 3380, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 3418, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 3456, .adv_w = 400, .box_w = 25, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 3494, .adv_w = 280, .box_w = 15, .box_h = 20, .ofs_x = 1, .ofs_y = -3}, + {.bitmap_index = 3532, .adv_w = 200, .box_w = 11, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 3561, .adv_w = 280, .box_w = 16, .box_h = 19, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3599, .adv_w = 400, .box_w = 25, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 3665, .adv_w = 360, .box_w = 23, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 3714, .adv_w = 320, .box_w = 20, .box_h = 20, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3764, .adv_w = 400, .box_w = 25, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3824, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 3877, .adv_w = 360, .box_w = 22, .box_h = 20, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3932, .adv_w = 360, .box_w = 22, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3985, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0} }; /*--------------------- @@ -1176,10 +1202,11 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_2[] = { - 0x0, 0x16, 0x26, 0x27, 0x28, 0x39, 0x47, 0x4a, - 0x4b, 0x50, 0x68, 0x94, 0x128, 0x184, 0x1e5, 0x1fb, - 0x21d, 0x23f, 0x240, 0x241, 0x242, 0x243, 0x292, 0x293, - 0x3dc, 0x3fc, 0x45c, 0x54a, 0x55f, 0x59e, 0x59f, 0x6a8 + 0x0, 0x16, 0x23, 0x26, 0x27, 0x28, 0x39, 0x47, + 0x4a, 0x4b, 0x4c, 0x50, 0x68, 0x94, 0x128, 0x184, + 0x1e5, 0x1fb, 0x21d, 0x23f, 0x240, 0x241, 0x242, 0x243, + 0x292, 0x293, 0x2f1, 0x3dc, 0x3fc, 0x45c, 0x54a, 0x55f, + 0x59e, 0x59f, 0x6a8 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1195,7 +1222,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = }, { .range_start = 61441, .range_length = 1705, .glyph_id_start = 160, - .unicode_list = unicode_list_2, .glyph_id_ofs_list = NULL, .list_length = 32, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_2, .glyph_id_ofs_list = NULL, .list_length = 35, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/src/displayapp/screens/ApplicationList.cpp b/src/displayapp/screens/ApplicationList.cpp index 0f3286df..dd9cb2a8 100644 --- a/src/displayapp/screens/ApplicationList.cpp +++ b/src/displayapp/screens/ApplicationList.cpp @@ -62,11 +62,11 @@ std::unique_ptr ApplicationList::CreateScreen1() { std::unique_ptr ApplicationList::CreateScreen2() { std::array applications { {{Symbols::map, Apps::Navigation}, - {Symbols::asterisk, Apps::Meter}, + {Symbols::stopWatch, Apps::StopWatch}, {Symbols::paintbrush, Apps::Paint}, - {Symbols::info, Apps::Notifications}, - {Symbols::paddle, Apps::Paddle}, - {"2", Apps::Twos} + {Symbols::info, Apps::Notifications}, + {Symbols::paddle, Apps::Paddle}, + {"2", Apps::Twos} } }; diff --git a/src/displayapp/screens/StopWatch.cpp b/src/displayapp/screens/StopWatch.cpp new file mode 100644 index 00000000..63f18d4b --- /dev/null +++ b/src/displayapp/screens/StopWatch.cpp @@ -0,0 +1,210 @@ +#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 milliSecs = (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, milliSecs}; + } + + 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_extrabold_compressed); + lv_obj_align(time, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, -45); + lv_label_set_text(time, "00:00"); + + 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_align(msecTime, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 108, 3); + lv_label_set_text(msecTime, "00"); + + 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_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_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) { + lv_obj_del(btnStopLap); + } + // 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.msecs); + + if (lapPressed == true) { + if (lapBuffer[1]) { + lv_label_set_text_fmt(lapOneText, "#%d %d:%d:%d", (lapNr - 1), lapBuffer[1]->mins, lapBuffer[1]->secs, lapBuffer[1]->msecs); + } + if (lapBuffer[0]) { + lv_label_set_text_fmt(lapTwoText, "#%d %d:%d:%d", lapNr, lapBuffer[0]->mins, lapBuffer[0]->secs, lapBuffer[0]->msecs); + } + // 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; + } + 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; + } + break; + } + } + return running; +} + +bool StopWatch::OnButtonPushed() { + running = false; + return true; +} + +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. + } + } +} diff --git a/src/displayapp/screens/StopWatch.h b/src/displayapp/screens/StopWatch.h new file mode 100644 index 00000000..f9dd5c76 --- /dev/null +++ b/src/displayapp/screens/StopWatch.h @@ -0,0 +1,86 @@ +#pragma once + +#include "Screen.h" +#include "components/datetime/DateTimeController.h" +#include "../LittleVgl.h" + +#include "FreeRTOS.h" +#include "portmacro_cmsis.h" + +#include + +namespace Pinetime::Applications::Screens { + + enum class States { Init, Running, Halted }; + + enum class Events { Play, Pause, Stop }; + + struct TimeSeparated_t { + int mins; + int secs; + int msecs; + }; + + // A simple buffer to hold the latest two laps + template struct LapTextBuffer_t { + LapTextBuffer_t() : buffer {}, currentSize {}, capacity {N}, head {-1} { + } + + void addLaps(const TimeSeparated_t& timeVal) { + head++; + head %= capacity; + buffer[head] = timeVal; + + if (currentSize < capacity) { + currentSize++; + } + } + + void clearBuffer() { + buffer = {}; + currentSize = 0; + head = -1; + } + + TimeSeparated_t* operator[](std::size_t idx) { + // Sanity check for out-of-bounds + if (idx >= 0 && idx < capacity) { + if (idx < currentSize) { + // This transformation is to ensure that head is always pointing to index 0. + const auto transformed_idx = (head - idx) % capacity; + return (&buffer[transformed_idx]); + } + } + return nullptr; + } + + private: + std::array buffer; + uint8_t currentSize; + uint8_t capacity; + int8_t head; + }; + + class StopWatch : public Screen { + public: + StopWatch(DisplayApp* app); + ~StopWatch() override; + bool Refresh() override; + bool OnButtonPushed() override; + void playPauseBtnEventHandler(lv_event_t event); + void stopLapBtnEventHandler(lv_event_t event); + + private: + bool running; + States currentState; + Events currentEvent; + TickType_t startTime; + TickType_t oldTimeElapsed; + TimeSeparated_t currentTimeSeparated; // Holds Mins, Secs, millisecs + LapTextBuffer_t<2> lapBuffer; + int lapNr; + bool lapPressed; + lv_obj_t *time, *msecTime, *btnPlayPause, *btnStopLap, *txtPlayPause, *txtStopLap; + lv_obj_t *lapOneText, *lapTwoText; + }; +} diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index 1a6bbd7f..9a13a755 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -36,6 +36,9 @@ namespace Pinetime { static constexpr const char* stepBackward = "\xEF\x81\x88"; static constexpr const char* play = "\xEF\x81\x8B"; static constexpr const char* pause = "\xEF\x81\x8C"; + static constexpr const char* stop = "\xEF\x81\x8D"; + static constexpr const char* stopWatch = "\xEF\x8B\xB2"; + static constexpr const char* lapsFlag = "\xEF\x80\xA4"; } } }