From c1628a9c7fa61e16b20ae5b7133456af8f3ede92 Mon Sep 17 00:00:00 2001 From: Auke Kok Date: Sat, 21 Feb 2026 15:22:35 -0800 Subject: [PATCH] Add Railway watch face Swiss/Dutch railway station clock-inspired analog watch face with 12 bold hour notches, thick hour and minute hands (no second hand), and a tap-to-toggle overlay that shows the full Digital watch face. The overlay auto-dismisses after 5 seconds. Co-Authored-By: Claude Opus 4.6 --- src/CMakeLists.txt | 1 + src/displayapp/UserApps.h | 1 + src/displayapp/apps/Apps.h.in | 1 + src/displayapp/apps/CMakeLists.txt | 1 + src/displayapp/screens/WatchFaceRailway.cpp | 169 ++++++++++++++++++++ src/displayapp/screens/WatchFaceRailway.h | 71 ++++++++ 6 files changed, 244 insertions(+) create mode 100644 src/displayapp/screens/WatchFaceRailway.cpp create mode 100644 src/displayapp/screens/WatchFaceRailway.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e4a354df64..5e4c8c117a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -430,6 +430,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp displayapp/screens/WatchFacePrideFlag.cpp + displayapp/screens/WatchFaceRailway.cpp ## diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 25926edc40..261ec04e3d 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -15,6 +15,7 @@ #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" #include "displayapp/screens/WatchFacePrideFlag.h" +#include "displayapp/screens/WatchFaceRailway.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index d440b598d1..b31207877b 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -56,6 +56,7 @@ namespace Pinetime { Infineat, CasioStyleG7710, PrideFlag, + Railway, }; template diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..792882bba0 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -29,6 +29,7 @@ else() 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::PrideFlag") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Railway") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/screens/WatchFaceRailway.cpp b/src/displayapp/screens/WatchFaceRailway.cpp new file mode 100644 index 0000000000..93f7af03bb --- /dev/null +++ b/src/displayapp/screens/WatchFaceRailway.cpp @@ -0,0 +1,169 @@ +#include "displayapp/screens/WatchFaceRailway.h" +#include "displayapp/screens/WatchFaceDigital.h" +#include +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/ble/NotificationManager.h" +#include "components/heartrate/HeartRateController.h" +#include "components/motion/MotionController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/settings/Settings.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + constexpr int16_t HourHandLength = 60; + constexpr int16_t MinuteHandLength = 85; +} + +WatchFaceRailway::WatchFaceRailway(AppControllers& controllers) + : currentDateTime {{}}, digitalOverlay {nullptr}, overlayDismissTask {nullptr}, controllers {controllers} { + + sHour = 99; + sMinute = 99; + + CreateAnalogFace(); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +void WatchFaceRailway::CreateAnalogFace() { + // 12 hour notches + hourNotchMeter = lv_linemeter_create(lv_scr_act(), nullptr); + lv_linemeter_set_scale(hourNotchMeter, 330, 12); + lv_linemeter_set_angle_offset(hourNotchMeter, 165); + lv_linemeter_set_value(hourNotchMeter, 0); + lv_obj_set_size(hourNotchMeter, 240, 240); + lv_obj_align(hourNotchMeter, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_bg_opa(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_scale_width(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 15); + lv_obj_set_style_local_scale_end_line_width(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6); + lv_obj_set_style_local_scale_end_color(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + + // Minute hand + minuteHandMeter = lv_linemeter_create(lv_scr_act(), nullptr); + lv_linemeter_set_scale(minuteHandMeter, 0, 2); + lv_linemeter_set_angle_offset(minuteHandMeter, 0); + lv_linemeter_set_value(minuteHandMeter, 0); + lv_obj_set_size(minuteHandMeter, MinuteHandLength * 2, MinuteHandLength * 2); + lv_obj_align(minuteHandMeter, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_bg_opa(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_scale_width(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, MinuteHandLength); + lv_obj_set_style_local_scale_end_line_width(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6); + lv_obj_set_style_local_scale_end_color(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + + // Hour hand (slightly wider) + hourHandMeter = lv_linemeter_create(lv_scr_act(), nullptr); + lv_linemeter_set_scale(hourHandMeter, 0, 2); + lv_linemeter_set_angle_offset(hourHandMeter, 0); + lv_linemeter_set_value(hourHandMeter, 0); + lv_obj_set_size(hourHandMeter, HourHandLength * 2, HourHandLength * 2); + lv_obj_align(hourHandMeter, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_bg_opa(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_scale_width(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, HourHandLength); + lv_obj_set_style_local_scale_end_line_width(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 8); + lv_obj_set_style_local_scale_end_color(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + + // Center dot + centerDot = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_size(centerDot, 12, 12); + lv_obj_align(centerDot, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_bg_color(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_radius(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); + lv_obj_set_style_local_border_width(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + + // Force hand positions + sHour = 99; + sMinute = 99; +} + +WatchFaceRailway::~WatchFaceRailway() { + lv_task_del(taskRefresh); + if (overlayDismissTask != nullptr) { + lv_task_del(overlayDismissTask); + } + if (digitalOverlay) { + delete digitalOverlay; + } else { + lv_obj_clean(lv_scr_act()); + } +} + +void WatchFaceRailway::UpdateClock() { + uint8_t hour = controllers.dateTimeController.Hours(); + uint8_t minute = controllers.dateTimeController.Minutes(); + + if (sMinute != minute) { + lv_linemeter_set_angle_offset(minuteHandMeter, minute * 6); + } + + if (sHour != hour || sMinute != minute) { + sHour = hour; + sMinute = minute; + lv_linemeter_set_angle_offset(hourHandMeter, hour * 30 + minute / 2); + } +} + +void WatchFaceRailway::Refresh() { + if (digitalOverlay == nullptr) { + currentDateTime = controllers.dateTimeController.CurrentDateTime(); + if (currentDateTime.IsUpdated()) { + UpdateClock(); + } + } +} + +bool WatchFaceRailway::OnTouchEvent(TouchEvents event) { + if (event == TouchEvents::Tap) { + if (digitalOverlay) { + HideOverlay(); + } else { + ShowOverlay(); + } + return true; + } + return false; +} + +void WatchFaceRailway::ShowOverlay() { + // Clear analog face before showing digital + lv_obj_clean(lv_scr_act()); + + digitalOverlay = new WatchFaceDigital(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.alarmController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + *controllers.weatherController); + + if (overlayDismissTask != nullptr) { + lv_task_del(overlayDismissTask); + } + overlayDismissTask = lv_task_create(DismissOverlayCallback, 5000, LV_TASK_PRIO_MID, this); + lv_task_set_repeat_count(overlayDismissTask, 1); +} + +void WatchFaceRailway::HideOverlay() { + if (overlayDismissTask != nullptr) { + lv_task_del(overlayDismissTask); + overlayDismissTask = nullptr; + } + + // Digital's destructor cleans all screen objects + delete digitalOverlay; + digitalOverlay = nullptr; + + // Recreate analog face + CreateAnalogFace(); + UpdateClock(); +} + +void WatchFaceRailway::DismissOverlayCallback(lv_task_t* task) { + auto* watchface = static_cast(task->user_data); + watchface->overlayDismissTask = nullptr; + watchface->HideOverlay(); +} diff --git a/src/displayapp/screens/WatchFaceRailway.h b/src/displayapp/screens/WatchFaceRailway.h new file mode 100644 index 0000000000..7e7a41af59 --- /dev/null +++ b/src/displayapp/screens/WatchFaceRailway.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + class WatchFaceDigital; + + class WatchFaceRailway : public Screen { + public: + WatchFaceRailway(AppControllers& controllers); + + ~WatchFaceRailway() override; + + void Refresh() override; + bool OnTouchEvent(TouchEvents event) override; + + private: + uint8_t sHour, sMinute; + + Utility::DirtyValue> currentDateTime; + + // 12 hour notch marks (linemeter) + lv_obj_t* hourNotchMeter; + + // Hands (linemeter, rotated via angle_offset) + lv_obj_t* hourHandMeter; + lv_obj_t* minuteHandMeter; + + // Center dot + lv_obj_t* centerDot; + + // Digital overlay + WatchFaceDigital* digitalOverlay; + lv_task_t* overlayDismissTask; + + AppControllers& controllers; + + void CreateAnalogFace(); + void UpdateClock(); + void ShowOverlay(); + void HideOverlay(); + static void DismissOverlayCallback(lv_task_t* task); + + lv_task_t* taskRefresh; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Railway; + static constexpr const char* name = "Railway"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceRailway(controllers); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +}