Skip to content

Commit 8efbebf

Browse files
sofarclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 6572b50 commit 8efbebf

File tree

6 files changed

+257
-0
lines changed

6 files changed

+257
-0
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ list(APPEND SOURCE_FILES
430430
displayapp/screens/WatchFacePineTimeStyle.cpp
431431
displayapp/screens/WatchFaceCasioStyleG7710.cpp
432432
displayapp/screens/WatchFacePrideFlag.cpp
433+
displayapp/screens/WatchFaceRailway.cpp
433434

434435
##
435436

src/displayapp/UserApps.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "displayapp/screens/WatchFacePineTimeStyle.h"
1616
#include "displayapp/screens/WatchFaceTerminal.h"
1717
#include "displayapp/screens/WatchFacePrideFlag.h"
18+
#include "displayapp/screens/WatchFaceRailway.h"
1819

1920
namespace Pinetime {
2021
namespace Applications {

src/displayapp/apps/Apps.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ namespace Pinetime {
5656
Infineat,
5757
CasioStyleG7710,
5858
PrideFlag,
59+
Railway,
5960
};
6061

6162
template <Apps>

src/displayapp/apps/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ else()
2929
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat")
3030
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710")
3131
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PrideFlag")
32+
set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Railway")
3233
set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware")
3334
endif()
3435

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#include "displayapp/screens/WatchFaceRailway.h"
2+
#include "displayapp/screens/WatchFaceDigital.h"
3+
#include <lvgl/lvgl.h>
4+
#include "components/battery/BatteryController.h"
5+
#include "components/ble/BleController.h"
6+
#include "components/ble/NotificationManager.h"
7+
#include "components/heartrate/HeartRateController.h"
8+
#include "components/motion/MotionController.h"
9+
#include "components/ble/SimpleWeatherService.h"
10+
#include "components/settings/Settings.h"
11+
12+
using namespace Pinetime::Applications::Screens;
13+
14+
namespace {
15+
constexpr int16_t HourHandLength = 60;
16+
constexpr int16_t MinuteHandLength = 85;
17+
}
18+
19+
WatchFaceRailway::WatchFaceRailway(AppControllers& controllers)
20+
: currentDateTime {{}}, digitalOverlay {nullptr}, overlayDismissTask {nullptr}, controllers {controllers} {
21+
22+
sHour = 99;
23+
sMinute = 99;
24+
25+
CreateAnalogFace();
26+
27+
taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
28+
Refresh();
29+
}
30+
31+
void WatchFaceRailway::CreateAnalogFace() {
32+
// 12 hour notches
33+
hourNotchMeter = lv_linemeter_create(lv_scr_act(), nullptr);
34+
lv_linemeter_set_scale(hourNotchMeter, 330, 12);
35+
lv_linemeter_set_angle_offset(hourNotchMeter, 165);
36+
lv_linemeter_set_value(hourNotchMeter, 0);
37+
lv_obj_set_size(hourNotchMeter, 240, 240);
38+
lv_obj_align(hourNotchMeter, nullptr, LV_ALIGN_CENTER, 0, 0);
39+
lv_obj_set_style_local_bg_opa(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP);
40+
lv_obj_set_style_local_scale_width(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 15);
41+
lv_obj_set_style_local_scale_end_line_width(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6);
42+
lv_obj_set_style_local_scale_end_color(hourNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
43+
44+
// Cardinal (12, 3, 6, 9) notches — 25% longer
45+
cardinalNotchMeter = lv_linemeter_create(lv_scr_act(), nullptr);
46+
lv_linemeter_set_scale(cardinalNotchMeter, 270, 4);
47+
lv_linemeter_set_angle_offset(cardinalNotchMeter, 135);
48+
lv_linemeter_set_value(cardinalNotchMeter, 0);
49+
lv_obj_set_size(cardinalNotchMeter, 240, 240);
50+
lv_obj_align(cardinalNotchMeter, nullptr, LV_ALIGN_CENTER, 0, 0);
51+
lv_obj_set_style_local_bg_opa(cardinalNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP);
52+
lv_obj_set_style_local_scale_width(cardinalNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 19);
53+
lv_obj_set_style_local_scale_end_line_width(cardinalNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6);
54+
lv_obj_set_style_local_scale_end_color(cardinalNotchMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
55+
56+
// Minute hand
57+
minuteHandMeter = lv_linemeter_create(lv_scr_act(), nullptr);
58+
lv_linemeter_set_scale(minuteHandMeter, 0, 2);
59+
lv_linemeter_set_angle_offset(minuteHandMeter, 0);
60+
lv_linemeter_set_value(minuteHandMeter, 0);
61+
lv_obj_set_size(minuteHandMeter, MinuteHandLength * 2, MinuteHandLength * 2);
62+
lv_obj_align(minuteHandMeter, nullptr, LV_ALIGN_CENTER, 0, 0);
63+
lv_obj_set_style_local_bg_opa(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP);
64+
lv_obj_set_style_local_scale_width(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, MinuteHandLength);
65+
lv_obj_set_style_local_scale_end_line_width(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6);
66+
lv_obj_set_style_local_scale_end_color(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
67+
68+
// Hour hand (slightly wider)
69+
hourHandMeter = lv_linemeter_create(lv_scr_act(), nullptr);
70+
lv_linemeter_set_scale(hourHandMeter, 0, 2);
71+
lv_linemeter_set_angle_offset(hourHandMeter, 0);
72+
lv_linemeter_set_value(hourHandMeter, 0);
73+
lv_obj_set_size(hourHandMeter, HourHandLength * 2, HourHandLength * 2);
74+
lv_obj_align(hourHandMeter, nullptr, LV_ALIGN_CENTER, 0, 0);
75+
lv_obj_set_style_local_bg_opa(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP);
76+
lv_obj_set_style_local_scale_width(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, HourHandLength);
77+
lv_obj_set_style_local_scale_end_line_width(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 8);
78+
lv_obj_set_style_local_scale_end_color(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
79+
80+
// Center dot
81+
centerDot = lv_obj_create(lv_scr_act(), nullptr);
82+
lv_obj_set_size(centerDot, 12, 12);
83+
lv_obj_align(centerDot, nullptr, LV_ALIGN_CENTER, 0, 0);
84+
lv_obj_set_style_local_bg_color(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
85+
lv_obj_set_style_local_radius(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
86+
lv_obj_set_style_local_border_width(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0);
87+
88+
// Force hand positions
89+
sHour = 99;
90+
sMinute = 99;
91+
}
92+
93+
WatchFaceRailway::~WatchFaceRailway() {
94+
lv_task_del(taskRefresh);
95+
if (overlayDismissTask != nullptr) {
96+
lv_task_del(overlayDismissTask);
97+
}
98+
if (digitalOverlay) {
99+
delete digitalOverlay;
100+
} else {
101+
lv_obj_clean(lv_scr_act());
102+
}
103+
}
104+
105+
void WatchFaceRailway::UpdateClock() {
106+
uint8_t hour = controllers.dateTimeController.Hours();
107+
uint8_t minute = controllers.dateTimeController.Minutes();
108+
109+
if (sMinute != minute) {
110+
lv_linemeter_set_angle_offset(minuteHandMeter, minute * 6);
111+
}
112+
113+
if (sHour != hour || sMinute != minute) {
114+
sHour = hour;
115+
sMinute = minute;
116+
lv_linemeter_set_angle_offset(hourHandMeter, hour * 30 + minute / 2);
117+
}
118+
}
119+
120+
void WatchFaceRailway::Refresh() {
121+
if (digitalOverlay == nullptr) {
122+
currentDateTime = controllers.dateTimeController.CurrentDateTime();
123+
if (currentDateTime.IsUpdated()) {
124+
UpdateClock();
125+
}
126+
}
127+
}
128+
129+
bool WatchFaceRailway::OnTouchEvent(TouchEvents event) {
130+
if (event == TouchEvents::Tap) {
131+
if (digitalOverlay) {
132+
HideOverlay();
133+
} else {
134+
ShowOverlay();
135+
}
136+
return true;
137+
}
138+
return false;
139+
}
140+
141+
void WatchFaceRailway::ShowOverlay() {
142+
// Clear analog face before showing digital
143+
lv_obj_clean(lv_scr_act());
144+
145+
digitalOverlay = new WatchFaceDigital(controllers.dateTimeController,
146+
controllers.batteryController,
147+
controllers.bleController,
148+
controllers.alarmController,
149+
controllers.notificationManager,
150+
controllers.settingsController,
151+
controllers.heartRateController,
152+
controllers.motionController,
153+
*controllers.weatherController);
154+
155+
if (overlayDismissTask != nullptr) {
156+
lv_task_del(overlayDismissTask);
157+
}
158+
overlayDismissTask = lv_task_create(DismissOverlayCallback, 5000, LV_TASK_PRIO_MID, this);
159+
lv_task_set_repeat_count(overlayDismissTask, 1);
160+
}
161+
162+
void WatchFaceRailway::HideOverlay() {
163+
if (overlayDismissTask != nullptr) {
164+
lv_task_del(overlayDismissTask);
165+
overlayDismissTask = nullptr;
166+
}
167+
168+
// Digital's destructor cleans all screen objects
169+
delete digitalOverlay;
170+
digitalOverlay = nullptr;
171+
172+
// Recreate analog face
173+
CreateAnalogFace();
174+
UpdateClock();
175+
}
176+
177+
void WatchFaceRailway::DismissOverlayCallback(lv_task_t* task) {
178+
auto* watchface = static_cast<WatchFaceRailway*>(task->user_data);
179+
watchface->overlayDismissTask = nullptr;
180+
watchface->HideOverlay();
181+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#pragma once
2+
3+
#include <lvgl/src/lv_core/lv_obj.h>
4+
#include <chrono>
5+
#include <cstdint>
6+
#include "displayapp/screens/Screen.h"
7+
#include "components/datetime/DateTimeController.h"
8+
#include "utility/DirtyValue.h"
9+
#include "displayapp/apps/Apps.h"
10+
#include "displayapp/Controllers.h"
11+
12+
namespace Pinetime {
13+
namespace Applications {
14+
namespace Screens {
15+
class WatchFaceDigital;
16+
17+
class WatchFaceRailway : public Screen {
18+
public:
19+
WatchFaceRailway(AppControllers& controllers);
20+
21+
~WatchFaceRailway() override;
22+
23+
void Refresh() override;
24+
bool OnTouchEvent(TouchEvents event) override;
25+
26+
private:
27+
uint8_t sHour, sMinute;
28+
29+
Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime;
30+
31+
// 12 hour notch marks (linemeter)
32+
lv_obj_t* hourNotchMeter;
33+
lv_obj_t* cardinalNotchMeter;
34+
35+
// Hands (linemeter, rotated via angle_offset)
36+
lv_obj_t* hourHandMeter;
37+
lv_obj_t* minuteHandMeter;
38+
39+
// Center dot
40+
lv_obj_t* centerDot;
41+
42+
// Digital overlay
43+
WatchFaceDigital* digitalOverlay;
44+
lv_task_t* overlayDismissTask;
45+
46+
AppControllers& controllers;
47+
48+
void CreateAnalogFace();
49+
void UpdateClock();
50+
void ShowOverlay();
51+
void HideOverlay();
52+
static void DismissOverlayCallback(lv_task_t* task);
53+
54+
lv_task_t* taskRefresh;
55+
};
56+
}
57+
58+
template <>
59+
struct WatchFaceTraits<WatchFace::Railway> {
60+
static constexpr WatchFace watchFace = WatchFace::Railway;
61+
static constexpr const char* name = "Railway";
62+
63+
static Screens::Screen* Create(AppControllers& controllers) {
64+
return new Screens::WatchFaceRailway(controllers);
65+
};
66+
67+
static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) {
68+
return true;
69+
}
70+
};
71+
}
72+
}

0 commit comments

Comments
 (0)