diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a01311b6..ed60cb00 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -434,6 +434,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFaceTerminal.cpp displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp + displayapp/screens/WatchFaceWordClock.cpp ## diff --git a/src/displayapp/screens/Clock.cpp b/src/displayapp/screens/Clock.cpp index 4219b090..6873f178 100644 --- a/src/displayapp/screens/Clock.cpp +++ b/src/displayapp/screens/Clock.cpp @@ -13,6 +13,7 @@ #include "displayapp/screens/WatchFaceAnalog.h" #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceCasioStyleG7710.h" +#include "displayapp/screens/WatchFaceWordClock.h" using namespace Pinetime::Applications::Screens; using namespace Pinetime::Applications; @@ -55,6 +56,9 @@ Clock::Clock(Controllers::DateTime& dateTimeController, case WatchFace::CasioStyleG7710: return WatchFaceCasioStyleG7710(); break; + case WatchFace::WatchFaceWordClock: + return WatchFaceWordClock(); + break; } return WatchFaceDigitalScreen(); }()} { @@ -131,3 +135,15 @@ std::unique_ptr Clock::WatchFaceCasioStyleG7710() { motionController, filesystem); } + +std::unique_ptr Clock::WatchFaceWordClock() { + return std::make_unique(dateTimeController, + batteryController, + bleController, + notificationManager, + settingsController, + heartRateController, + motionController, + filesystem); +} + diff --git a/src/displayapp/screens/Clock.h b/src/displayapp/screens/Clock.h index f3591f43..a31627e7 100644 --- a/src/displayapp/screens/Clock.h +++ b/src/displayapp/screens/Clock.h @@ -54,6 +54,7 @@ namespace Pinetime { std::unique_ptr WatchFaceTerminalScreen(); std::unique_ptr WatchFaceInfineatScreen(); std::unique_ptr WatchFaceCasioStyleG7710(); + std::unique_ptr WatchFaceWordClock(); }; } } diff --git a/src/displayapp/screens/WatchFaceWordClock.cpp b/src/displayapp/screens/WatchFaceWordClock.cpp new file mode 100644 index 00000000..ae3aad55 --- /dev/null +++ b/src/displayapp/screens/WatchFaceWordClock.cpp @@ -0,0 +1,311 @@ +#include "Clock.h" + +#include +#include +#include +#include "BatteryIcon.h" +#include "BleIcon.h" +#include "NotificationIcon.h" +#include "Symbols.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/ble/NotificationManager.h" +#include "components/heartrate/HeartRateController.h" +#include "../DisplayApp.h" + +using namespace Pinetime::Applications::Screens; + +static void event_handler(lv_obj_t * obj, lv_event_t event) { + Clock* screen = static_cast(obj->user_data); + screen->OnObjectEvent(obj, event); +} + +Clock::Clock(DisplayApp* app, + Controllers::DateTime& dateTimeController, + Controllers::Battery& batteryController, + Controllers::Ble& bleController, + Controllers::NotificationManager& notificatioManager, + Controllers::HeartRateController& heartRateController): Screen(app), currentDateTime{{}}, + dateTimeController{dateTimeController}, batteryController{batteryController}, + bleController{bleController}, notificatioManager{notificatioManager}, + heartRateController{heartRateController} { + displayedChar[0] = 0; + displayedChar[1] = 0; + displayedChar[2] = 0; + displayedChar[3] = 0; + displayedChar[4] = 0; + + batteryIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(batteryIcon, Symbols::batteryFull); + lv_obj_align(batteryIcon, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, -5, 2); + + batteryPlug = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(batteryPlug, Symbols::plug); + lv_obj_align(batteryPlug, batteryIcon, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + bleIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(bleIcon, Symbols::bluetooth); + lv_obj_align(bleIcon, batteryPlug, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + notificationIcon = lv_label_create(lv_scr_act(), NULL); + lv_label_set_text(notificationIcon, NotificationIcon::GetIcon(false)); + lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_TOP_LEFT, 10, 0); + + label_date = lv_label_create(lv_scr_act(), nullptr); + + lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 10, 60); + + label_time_shadow = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(label_time_shadow, true); + lv_obj_align(label_time_shadow, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 7, -23); + + label_time = lv_label_create(lv_scr_act(), nullptr); + + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 5, -25); + + backgroundLabel = lv_label_create(lv_scr_act(), nullptr); + backgroundLabel->user_data = this; + lv_obj_set_click(backgroundLabel, true); + lv_obj_set_event_cb(backgroundLabel, event_handler); + lv_label_set_long_mode(backgroundLabel, LV_LABEL_LONG_CROP); + lv_obj_set_size(backgroundLabel, 240, 240); + lv_obj_set_pos(backgroundLabel, 0, 0); + lv_label_set_text(backgroundLabel, ""); + + + heartbeatIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(heartbeatIcon, Symbols::heartBeat); + lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 5, -2); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(heartbeatValue, "0"); + lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + + heartbeatBpm = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(heartbeatBpm, "BPM"); + lv_obj_align(heartbeatBpm, heartbeatValue, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(stepValue, "0"); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, -5, -2); + + stepIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(stepIcon, Symbols::shoe); + lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); +} + +Clock::~Clock() { + lv_obj_clean(lv_scr_act()); +} + +bool Clock::Refresh() { + batteryPercentRemaining = batteryController.PercentRemaining(); + if (batteryPercentRemaining.IsUpdated()) { + auto batteryPercent = batteryPercentRemaining.Get(); + lv_label_set_text(batteryIcon, BatteryIcon::GetBatteryIcon(batteryPercent)); + auto isCharging = batteryController.IsCharging() || batteryController.IsPowerPresent(); + lv_label_set_text(batteryPlug, BatteryIcon::GetPlugIcon(isCharging)); + } + + bleState = bleController.IsConnected(); + if (bleState.IsUpdated()) { + if(bleState.Get() == true) { + lv_label_set_text(bleIcon, BleIcon::GetIcon(true)); + } else { + lv_label_set_text(bleIcon, BleIcon::GetIcon(false)); + } + } + lv_obj_align(batteryIcon, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, -5, 5); + lv_obj_align(batteryPlug, batteryIcon, LV_ALIGN_OUT_LEFT_MID, -5, 0); + lv_obj_align(bleIcon, batteryPlug, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + notificationState = notificatioManager.AreNewNotificationsAvailable(); + if(notificationState.IsUpdated()) { + if(notificationState.Get() == true) + lv_label_set_text(notificationIcon, NotificationIcon::GetIcon(true)); + else + lv_label_set_text(notificationIcon, NotificationIcon::GetIcon(false)); + } + + currentDateTime = dateTimeController.CurrentDateTime(); + + if(currentDateTime.IsUpdated()) { + auto newDateTime = currentDateTime.Get(); + + auto dp = date::floor(newDateTime); + auto time = date::make_time(newDateTime-dp); + auto yearMonthDay = date::year_month_day(dp); + + auto year = (int)yearMonthDay.year(); + auto month = static_cast((unsigned)yearMonthDay.month()); + auto day = (unsigned)yearMonthDay.day(); + auto dayOfWeek = static_cast(date::weekday(yearMonthDay).iso_encoding()); + + auto hour = time.hours().count(); + auto minute = time.minutes().count(); + + char minutesChar[3]; + sprintf(minutesChar, "%02d", static_cast(minute)); + + char hoursChar[3]; + sprintf(hoursChar, "%02d", static_cast(hour)); + + printwords(static_cast(hour), static_cast(minute)); + + if(hoursChar[0] != displayedChar[0] || hoursChar[1] != displayedChar[1] || minutesChar[0] != displayedChar[2] || minutesChar[1] != displayedChar[3]) { + displayedChar[0] = hoursChar[0]; + displayedChar[1] = hoursChar[1]; + displayedChar[2] = minutesChar[0]; + displayedChar[3] = minutesChar[1]; + + lv_label_set_text(label_time_shadow, timeStrShadow); + lv_label_set_text(label_time, timeStr); + + } + + if ((year != currentYear) || (month != currentMonth) || (dayOfWeek != currentDayOfWeek) || (day != currentDay)) { + char dateStr[22]; + sprintf(dateStr, "%s %d %s %d", DayOfWeekToString(dayOfWeek), day, MonthToString(month), year); + lv_label_set_text(label_date, dateStr); + + + currentYear = year; + currentMonth = month; + currentDayOfWeek = dayOfWeek; + currentDay = day; + } + } + + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if(heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + char heartbeatBuffer[4]; + if(heartbeatRunning.Get()) + sprintf(heartbeatBuffer, "%d", heartbeat.Get()); + else + sprintf(heartbeatBuffer, "---"); + + lv_label_set_text(heartbeatValue, heartbeatBuffer); + lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 5, -2); + lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + lv_obj_align(heartbeatBpm, heartbeatValue, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + } + + // TODO stepCount = stepController.GetValue(); + if(stepCount.IsUpdated()) { + char stepBuffer[5]; + sprintf(stepBuffer, "%lu", stepCount.Get()); + lv_label_set_text(stepValue, stepBuffer); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, -5, -2); + lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); + } + + return running; +} + +const char *Clock::MonthToString(Pinetime::Controllers::DateTime::Months month) { + return Clock::MonthsString[static_cast(month)]; +} + +const char *Clock::DayOfWeekToString(Pinetime::Controllers::DateTime::Days dayOfWeek) { + return Clock::DaysString[static_cast(dayOfWeek)]; +} + +char const *Clock::DaysString[] = { + "", + "mon.", + "tue.", + "wed.", + "thu.", + "fri.", + "sat.", + "sun." +}; + +char const *Clock::MonthsString[] = { + "", + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec" +}; + +char const *Clock::nums[] = { "zero", "one", "two", "three", "four", + "five", "six", "seven", "eight", "nine", + "ten", "eleven", "twelve", "thirteen", + "fourteen", "fifteen", "sixteen", "seventeen", + "eighteen", "nineteen", "twenty", "twenty one", + "twenty two", "twenty three", "twenty four", + "twenty five", "twenty six", "twenty seven", + "twenty eight", "twenty nine", + }; + +void Clock::OnObjectEvent(lv_obj_t *obj, lv_event_t event) { + if(obj == backgroundLabel) { + if (event == LV_EVENT_CLICKED) { + + running = false; + } + } +} + +void Clock::printwords(int h, int m) { + if (m == 0) { + sprintf(timeStrShadow, "#404040 %s#\n#404040 o' clock#\n", nums[h]); + sprintf(timeStr, "%s\no' clock\n", nums[h]); + + } + + else if (m == 1){ + sprintf(timeStrShadow, "#404040 one minute#\n#404040 past# #404040 %s#\n", nums[h]); + sprintf(timeStr, "one minute\npast %s\n", nums[h]); + + } + + else if (m == 59) { + sprintf(timeStrShadow, "#404040 one minute#\n#404040 to# #404040 %s#\n", nums[(h % 12) + 1]); + sprintf(timeStr, "one minute\nto %s\n", nums[(h % 12) + 1]); + + } + + else if (m == 15) { + sprintf(timeStrShadow, "#404040 quarter past#\n#404040 %s#\n", nums[h]); + sprintf(timeStr, "quarter past\n%s\n", nums[h]); + + } + + else if (m == 30) { + sprintf(timeStrShadow, "#404040 half past#\n#404040 %s#\n", nums[h]); + sprintf(timeStr, "half past\n%s\n", nums[h]); + + } + + else if (m == 45) { + sprintf(timeStr, "quarter to\n%s\n", nums[(h % 12) + 1]); + sprintf(timeStrShadow, "#404040 quarter to#\n#404040 %s#\n", nums[(h % 12) + 1]); + } + + else if (m <= 30) { + sprintf(timeStrShadow, "#404040 %s# #404040 minutes#\n#404040 past# #404040 %s#\n", nums[m], nums[h]); + sprintf(timeStr, "%s minutes\npast %s\n", nums[m], nums[h]); + } + + else if (m > 30){ + sprintf(timeStrShadow, "#404040 %s# #404040 minutes#\n#404040 to# #404040 %s#\n", nums[60 - m], nums[(h % 12) + 1]); + sprintf(timeStr, "%s minutes\nto %s\n", nums[60 - m], nums[(h % 12) + 1]); + + } +} + +bool Clock::OnButtonPushed() { + running = false; + return false; +} \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceWordClock.h b/src/displayapp/screens/WatchFaceWordClock.h new file mode 100644 index 00000000..70114b40 --- /dev/null +++ b/src/displayapp/screens/WatchFaceWordClock.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include "Screen.h" +#include "components/datetime/DateTimeController.h" + +namespace Pinetime { + namespace Controllers { + class Battery; + class Ble; + class NotificationManager; + class HeartRateController; + } + + namespace Applications { + namespace Screens { + + template + class DirtyValue { + public: + DirtyValue() = default; // Use NSDMI + explicit DirtyValue(T const& v):value{v}{} // Use MIL and const-lvalue-ref + bool IsUpdated() const { return isUpdated; } + T const& Get() { this->isUpdated = false; return value; } // never expose a non-const lvalue-ref + DirtyValue& operator=(const T& other) { + if (this->value != other) { + this->value = other; + this->isUpdated = true; + } + return *this; + } + private: + T value{}; // NSDMI - default initialise type + bool isUpdated{true}; // NSDMI - use brace initilisation + }; + + class Clock : public Screen { + public: + Clock(DisplayApp* app, + Controllers::DateTime& dateTimeController, + Controllers::Battery& batteryController, + Controllers::Ble& bleController, + Controllers::NotificationManager& notificatioManager, + Controllers::HeartRateController& heartRateController); + ~Clock() override; + + bool Refresh() override; + bool OnButtonPushed() override; + + void OnObjectEvent(lv_obj_t *pObj, lv_event_t i); + void printwords(int h, int m); + + private: + static const char* MonthToString(Pinetime::Controllers::DateTime::Months month); + static const char* DayOfWeekToString(Pinetime::Controllers::DateTime::Days dayOfWeek); + static char const *DaysString[]; + static char const *MonthsString[]; + static char const *nums[]; + + char displayedChar[5]; + char timeStr[64]; + char timeStrShadow[96]; + + uint16_t currentYear = 1970; + Pinetime::Controllers::DateTime::Months currentMonth = Pinetime::Controllers::DateTime::Months::Unknown; + Pinetime::Controllers::DateTime::Days currentDayOfWeek = Pinetime::Controllers::DateTime::Days::Unknown; + uint8_t currentDay = 0; + + DirtyValue batteryPercentRemaining {}; + DirtyValue bleState {}; + DirtyValue> currentDateTime{}; + DirtyValue stepCount {}; + DirtyValue heartbeat {}; + DirtyValue heartbeatRunning {}; + DirtyValue notificationState {}; + + lv_obj_t* label_time; + lv_obj_t* label_date; + lv_obj_t* label_time_shadow; + lv_obj_t* label_date_shadow; + lv_obj_t* backgroundLabel; + lv_obj_t* batteryIcon; + lv_obj_t* bleIcon; + lv_obj_t* batteryPlug; + lv_obj_t* heartbeatIcon; + lv_obj_t* heartbeatValue; + lv_obj_t* heartbeatBpm; + lv_obj_t* stepIcon; + lv_obj_t* stepValue; + lv_obj_t* notificationIcon; + + Controllers::DateTime& dateTimeController; + Controllers::Battery& batteryController; + Controllers::Ble& bleController; + Controllers::NotificationManager& notificatioManager; + Controllers::HeartRateController& heartRateController; + + bool running = true; + + }; + } + } +} \ No newline at end of file