diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64cbada3..b9c5f1b1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -432,6 +432,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFaceTerminal.cpp displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp + displayapp/screens/WatchFaceHorizon.cpp ## diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index c69cdba7..e51dc9da 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -15,6 +15,7 @@ #include "displayapp/screens/WatchFaceInfineat.h" #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" +#include "displayapp/screens/WatchFaceHorizon.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 1d78799c..64ad2572 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -57,6 +57,7 @@ namespace Pinetime { Terminal, Infineat, CasioStyleG7710, + Horizon }; template diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index f23c845a..b25f4846 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -30,6 +30,7 @@ else() set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Terminal") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Horizon") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/screens/WatchFaceHorizon.cpp b/src/displayapp/screens/WatchFaceHorizon.cpp new file mode 100644 index 00000000..fc9904c1 --- /dev/null +++ b/src/displayapp/screens/WatchFaceHorizon.cpp @@ -0,0 +1,250 @@ +#include "displayapp/screens/WatchFaceHorizon.h" + +#include +#include +#include "components/settings/Settings.h" +#include "components/battery/BatteryController.h" +#include "components/motion/MotionController.h" +#include "utility/DirtyValue.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceHorizon::WatchFaceHorizon(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + Controllers::Settings& settingsController, + Controllers::MotionController& motionController, + Controllers::FS& filesystem) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + batteryController {batteryController}, + settingsController {settingsController}, + motionController {motionController} { + lfs_file f = {}; + if (filesystem.FileOpen(&f, "/fonts/pinecone_28.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + font_pinecone_28 = lv_font_load("F:/fonts/pinecone_28.bin"); + } + + if (filesystem.FileOpen(&f, "/fonts/pinecone_70.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + font_pinecone_70 = lv_font_load("F:/fonts/pinecone_70.bin"); + } + + // Black background covering the whole screen + background = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_color(background, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_size(background, 240, 240); + lv_obj_align(background, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 0, 0); + + // Battery indicator line + lineBatteryBg = lv_line_create(lv_scr_act(), nullptr); + lineBatteryFg = lv_line_create(lv_scr_act(), nullptr); + + lv_style_init(&lineBatteryBgStyle); + lv_style_set_line_width(&lineBatteryBgStyle, LV_STATE_DEFAULT, 4); + lv_style_set_line_color(&lineBatteryBgStyle, LV_STATE_DEFAULT, lv_color_hex(0x181818)); + lv_obj_add_style(lineBatteryBg, LV_LINE_PART_MAIN, &lineBatteryBgStyle); + lineBatteryBgPoints[0] = {116, 40}; + lineBatteryBgPoints[1] = {116, 200}; + lv_line_set_points(lineBatteryBg, lineBatteryBgPoints, 2); + + lv_style_init(&lineBatteryFgStyle); + lv_style_set_line_width(&lineBatteryFgStyle, LV_STATE_DEFAULT, 4); + lv_style_set_line_color(&lineBatteryFgStyle, LV_STATE_DEFAULT, lv_color_hex(hourlyColors[0])); + lv_obj_add_style(lineBatteryFg, LV_LINE_PART_MAIN, &lineBatteryFgStyle); + lineBatteryFgPoints[0] = {116, 40}; + lineBatteryFgPoints[1] = {116, 200}; + lv_line_set_points(lineBatteryFg, lineBatteryFgPoints, 2); + + // Hour indicator lines at bottom + for (int i = 0; i < 24; i++) { + hourLines[i] = lv_line_create(lv_scr_act(), nullptr); + lv_style_init(&hourLineStyles[i]); + lv_style_set_line_width(&hourLineStyles[i], LV_STATE_DEFAULT, 5); + lv_style_set_line_color(&hourLineStyles[i], LV_STATE_DEFAULT, lv_color_hex(hourlyColors[i])); + lv_obj_add_style(hourLines[i], LV_LINE_PART_MAIN, &hourLineStyles[i]); + hourLinePoints[i][0] = {static_cast(i * 10), 237}; + hourLinePoints[i][1] = {static_cast((i + 1) * 10), 237}; + lv_line_set_points(hourLines[i], hourLinePoints[i], 2); + } + + // Hour (split digits due to non-monospaced font) + labelHourFirstDigit = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelHourFirstDigit, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_70); + lv_label_set_text(labelHourFirstDigit, "0"); + lv_obj_align(labelHourFirstDigit, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 130, -43); + + labelHourSecondDigit = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelHourSecondDigit, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_70); + lv_label_set_text(labelHourSecondDigit, "0"); + lv_obj_align(labelHourSecondDigit, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 174, -43); + + // Minutes (split digits due to non-monospaced font) + labelMinutesFirstDigit = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelMinutesFirstDigit, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_70); + lv_label_set_text(labelMinutesFirstDigit, "0"); + lv_obj_align(labelMinutesFirstDigit, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 130, 44); + + labelMinutesSecondDigit = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelMinutesSecondDigit, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_70); + lv_label_set_text(labelMinutesSecondDigit, "0"); + lv_obj_align(labelMinutesSecondDigit, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 174, 44); + + // Day of week + labelDayOfWeek = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(labelDayOfWeek, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xbebebe)); + lv_obj_set_style_local_text_font(labelDayOfWeek, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_28); + lv_obj_align(labelDayOfWeek, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -140, -60); + lv_label_set_text(labelDayOfWeek, "MON"); + + // Month + labelMonth = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelMonth, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_28); + lv_obj_align(labelMonth, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -140, -20); + lv_label_set_text(labelMonth, "JAN"); + + // Day of month + labelDate = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(labelDate, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xbebebe)); + lv_obj_set_style_local_text_font(labelDate, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_28); + lv_obj_align(labelDate, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -140, 20); + lv_label_set_text(labelDate, "01"); + + // Number of steps + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_pinecone_28); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -140, 60); + lv_label_set_text(stepValue, "0"); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceHorizon::~WatchFaceHorizon() { + lv_task_del(taskRefresh); + + // Reset styles + lv_style_reset(&lineBatteryFgStyle); + lv_style_reset(&lineBatteryBgStyle); + + for (int i = 0; i < 24; i++) { + lv_style_reset(&hourLineStyles[i]); + } + + // Free font resources + if (font_pinecone_28 != nullptr) { + lv_font_free(font_pinecone_28); + } + + if (font_pinecone_70 != nullptr) { + lv_font_free(font_pinecone_70); + } + + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceHorizon::Refresh() { + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + + if (currentDateTime.IsUpdated()) { + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + uint8_t day = dateTimeController.Day(); + + char minutesChar[3]; + sprintf(minutesChar, "%02d", static_cast(minute)); + + char hoursChar[3]; + int displayHour = hour; + + // Account for 12-hour time + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + if (hour < 12) { + if (hour == 0) { + displayHour = 12; + } + } else { + if (hour != 12) { + displayHour = hour - 12; + } + } + } + sprintf(hoursChar, "%02d", displayHour); + + // Hour has updated + if (hoursChar[0] != displayedChar[0] || hoursChar[1] != displayedChar[1]) { + displayedChar[0] = hoursChar[0]; + displayedChar[1] = hoursChar[1]; + + // Update colors appropriately + lv_obj_set_style_local_text_color(labelHourFirstDigit, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(hourlyColors[hour])); + lv_obj_set_style_local_text_color(labelHourSecondDigit, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(hourlyColors[hour])); + lv_obj_set_style_local_text_color(labelMonth, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(hourlyColors[hour])); + lv_style_set_line_color(&lineBatteryFgStyle, LV_STATE_DEFAULT, lv_color_hex(hourlyColors[hour])); + + lv_label_set_text_fmt(labelHourFirstDigit, "%c", hoursChar[0]); + lv_label_set_text_fmt(labelHourSecondDigit, "%c", hoursChar[1]); + + // Update hour bar on bottom + for (int i = 0; i < 24; i++) { + if (i <= hour) { + lv_obj_set_hidden(hourLines[i], false); + } else { + lv_obj_set_hidden(hourLines[i], true); + } + } + } + + // Minutes have updated + if (minutesChar[0] != displayedChar[2] || minutesChar[1] != displayedChar[3]) { + displayedChar[2] = minutesChar[0]; + displayedChar[3] = minutesChar[1]; + + lv_label_set_text_fmt(labelMinutesFirstDigit, "%c", minutesChar[0]); + lv_label_set_text_fmt(labelMinutesSecondDigit, "%c", minutesChar[1]); + } + + // Date has updated + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + lv_label_set_text_fmt(labelDayOfWeek, "%s", dateTimeController.DayOfWeekShortToString()); + lv_label_set_text_fmt(labelMonth, "%s", dateTimeController.MonthShortToString()); + lv_label_set_text_fmt(labelDate, "%d", day); + + lv_obj_realign(labelDayOfWeek); + lv_obj_realign(labelMonth); + lv_obj_realign(labelDate); + } + } + + // Set battery line + batteryPercentRemaining = batteryController.PercentRemaining(); + if (batteryPercentRemaining.IsUpdated()) { + lineBatteryFgPoints[0] = {116, static_cast(200 - (1.6 * batteryPercentRemaining.Get()))}; + lv_line_set_points(lineBatteryFg, lineBatteryFgPoints, 2); + } + + // Set step count + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + lv_obj_realign(stepValue); + } +} + +bool WatchFaceHorizon::IsAvailable(Pinetime::Controllers::FS& filesystem) { + lfs_file file = {}; + + if (filesystem.FileOpen(&file, "/fonts/pinecone_28.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + if (filesystem.FileOpen(&file, "/fonts/pinecone_70.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + return true; +} diff --git a/src/displayapp/screens/WatchFaceHorizon.h b/src/displayapp/screens/WatchFaceHorizon.h new file mode 100644 index 00000000..c909255a --- /dev/null +++ b/src/displayapp/screens/WatchFaceHorizon.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/BleController.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceHorizon : public Screen { + public: + WatchFaceHorizon(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + Controllers::Settings& settingsController, + Controllers::MotionController& motionController, + Controllers::FS& fs); + + ~WatchFaceHorizon() override; + + void Refresh() override; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem); + + private: + char displayedChar[5] {}; + + 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; + Utility::DirtyValue> currentDate; + + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue motionSensorOk {}; + Utility::DirtyValue stepCount {}; + + lv_obj_t* background; + + lv_obj_t* hourLines[24]; + lv_style_t hourLineStyles[24]; + lv_point_t hourLinePoints[24][2]; + + lv_obj_t* labelHourFirstDigit; + lv_obj_t* labelHourSecondDigit; + + lv_obj_t* labelMinutesFirstDigit; + lv_obj_t* labelMinutesSecondDigit; + + lv_obj_t* lineBatteryFg; + lv_obj_t* lineBatteryBg; + lv_style_t lineBatteryFgStyle; + lv_style_t lineBatteryBgStyle; + lv_point_t lineBatteryFgPoints[2]; + lv_point_t lineBatteryBgPoints[2]; + + lv_obj_t* labelDayOfWeek; + lv_obj_t* labelMonth; + lv_obj_t* labelDate; + + lv_obj_t* stepValue; + + Controllers::DateTime& dateTimeController; + const Controllers::Battery& batteryController; + Controllers::Settings& settingsController; + Controllers::MotionController& motionController; + + lv_task_t* taskRefresh; + lv_font_t* font_pinecone_28 = nullptr; + lv_font_t* font_pinecone_70 = nullptr; + + int hourlyColors[24] = {0x353f76, 0x415587, 0x4e6a98, 0x5a80a9, 0x6696ba, 0x7fa5b1, 0x98b5a7, 0xb0c49e, + 0xc9d494, 0xe2e38b, 0xe3d780, 0xe4ca75, 0xe5be69, 0xe6b15e, 0xe7a553, 0xd29357, + 0xbd815b, 0xa86f60, 0x935d64, 0x7e4b68, 0x6d4467, 0x4b3766, 0x3a3066, 0x292965}; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Horizon; + static constexpr const char* name = "Horizon"; + + static Screens::Screen* Create;/*(AppControllers& controllers) { + return new Screens::WatchFaceHorizon(controllers.dateTimeController, + controllers.batteryController, + controllers.settingsController, + controllers.motionController, + controllers.filesystem); + };*/ + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem) { + return Screens::WatchFaceHorizon::IsAvailable(filesystem); + } + }; + } +} diff --git a/src/displayapp/screens/settings/SettingWatchFace.h b/src/displayapp/screens/settings/SettingWatchFace.h index 4c75b0ab..8acb65b3 100644 --- a/src/displayapp/screens/settings/SettingWatchFace.h +++ b/src/displayapp/screens/settings/SettingWatchFace.h @@ -11,6 +11,7 @@ #include "displayapp/screens/CheckboxList.h" #include "displayapp/screens/WatchFaceInfineat.h" #include "displayapp/screens/WatchFaceCasioStyleG7710.h" +#include "displayapp/screens/WatchFaceHorizon.h" namespace Pinetime { diff --git a/src/resources/fonts.json b/src/resources/fonts.json index c4a63349..05d0fc4f 100644 --- a/src/resources/fonts.json +++ b/src/resources/fonts.json @@ -58,5 +58,29 @@ "size": 115, "format": "bin", "target_path": "/fonts/" - } + }, + "pinecone_28" : { + "sources": [ + { + "file": "fonts/Pinecone-Regular.ttf", + "symbols": "0123456789MONTUEWDHFRISAJBPYLGCV" + } + ], + "bpp": 1, + "size": 28, + "format": "bin", + "target_path": "/fonts/" + }, + "pinecone_70" : { + "sources": [ + { + "file": "fonts/Pinecone-Regular.ttf", + "symbols": "0123456789" + } + ], + "bpp": 1, + "size": 70, + "format": "bin", + "target_path": "/fonts/" + } } diff --git a/src/resources/fonts/Pinecone-Regular.ttf b/src/resources/fonts/Pinecone-Regular.ttf new file mode 100644 index 00000000..b20e57c0 Binary files /dev/null and b/src/resources/fonts/Pinecone-Regular.ttf differ