Skip to content

Commit c1628a9

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 ad73e1b commit c1628a9

6 files changed

Lines changed: 244 additions & 0 deletions

File tree

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: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
// Minute hand
45+
minuteHandMeter = lv_linemeter_create(lv_scr_act(), nullptr);
46+
lv_linemeter_set_scale(minuteHandMeter, 0, 2);
47+
lv_linemeter_set_angle_offset(minuteHandMeter, 0);
48+
lv_linemeter_set_value(minuteHandMeter, 0);
49+
lv_obj_set_size(minuteHandMeter, MinuteHandLength * 2, MinuteHandLength * 2);
50+
lv_obj_align(minuteHandMeter, nullptr, LV_ALIGN_CENTER, 0, 0);
51+
lv_obj_set_style_local_bg_opa(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP);
52+
lv_obj_set_style_local_scale_width(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, MinuteHandLength);
53+
lv_obj_set_style_local_scale_end_line_width(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6);
54+
lv_obj_set_style_local_scale_end_color(minuteHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
55+
56+
// Hour hand (slightly wider)
57+
hourHandMeter = lv_linemeter_create(lv_scr_act(), nullptr);
58+
lv_linemeter_set_scale(hourHandMeter, 0, 2);
59+
lv_linemeter_set_angle_offset(hourHandMeter, 0);
60+
lv_linemeter_set_value(hourHandMeter, 0);
61+
lv_obj_set_size(hourHandMeter, HourHandLength * 2, HourHandLength * 2);
62+
lv_obj_align(hourHandMeter, nullptr, LV_ALIGN_CENTER, 0, 0);
63+
lv_obj_set_style_local_bg_opa(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP);
64+
lv_obj_set_style_local_scale_width(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, HourHandLength);
65+
lv_obj_set_style_local_scale_end_line_width(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 8);
66+
lv_obj_set_style_local_scale_end_color(hourHandMeter, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
67+
68+
// Center dot
69+
centerDot = lv_obj_create(lv_scr_act(), nullptr);
70+
lv_obj_set_size(centerDot, 12, 12);
71+
lv_obj_align(centerDot, nullptr, LV_ALIGN_CENTER, 0, 0);
72+
lv_obj_set_style_local_bg_color(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
73+
lv_obj_set_style_local_radius(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
74+
lv_obj_set_style_local_border_width(centerDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0);
75+
76+
// Force hand positions
77+
sHour = 99;
78+
sMinute = 99;
79+
}
80+
81+
WatchFaceRailway::~WatchFaceRailway() {
82+
lv_task_del(taskRefresh);
83+
if (overlayDismissTask != nullptr) {
84+
lv_task_del(overlayDismissTask);
85+
}
86+
if (digitalOverlay) {
87+
delete digitalOverlay;
88+
} else {
89+
lv_obj_clean(lv_scr_act());
90+
}
91+
}
92+
93+
void WatchFaceRailway::UpdateClock() {
94+
uint8_t hour = controllers.dateTimeController.Hours();
95+
uint8_t minute = controllers.dateTimeController.Minutes();
96+
97+
if (sMinute != minute) {
98+
lv_linemeter_set_angle_offset(minuteHandMeter, minute * 6);
99+
}
100+
101+
if (sHour != hour || sMinute != minute) {
102+
sHour = hour;
103+
sMinute = minute;
104+
lv_linemeter_set_angle_offset(hourHandMeter, hour * 30 + minute / 2);
105+
}
106+
}
107+
108+
void WatchFaceRailway::Refresh() {
109+
if (digitalOverlay == nullptr) {
110+
currentDateTime = controllers.dateTimeController.CurrentDateTime();
111+
if (currentDateTime.IsUpdated()) {
112+
UpdateClock();
113+
}
114+
}
115+
}
116+
117+
bool WatchFaceRailway::OnTouchEvent(TouchEvents event) {
118+
if (event == TouchEvents::Tap) {
119+
if (digitalOverlay) {
120+
HideOverlay();
121+
} else {
122+
ShowOverlay();
123+
}
124+
return true;
125+
}
126+
return false;
127+
}
128+
129+
void WatchFaceRailway::ShowOverlay() {
130+
// Clear analog face before showing digital
131+
lv_obj_clean(lv_scr_act());
132+
133+
digitalOverlay = new WatchFaceDigital(controllers.dateTimeController,
134+
controllers.batteryController,
135+
controllers.bleController,
136+
controllers.alarmController,
137+
controllers.notificationManager,
138+
controllers.settingsController,
139+
controllers.heartRateController,
140+
controllers.motionController,
141+
*controllers.weatherController);
142+
143+
if (overlayDismissTask != nullptr) {
144+
lv_task_del(overlayDismissTask);
145+
}
146+
overlayDismissTask = lv_task_create(DismissOverlayCallback, 5000, LV_TASK_PRIO_MID, this);
147+
lv_task_set_repeat_count(overlayDismissTask, 1);
148+
}
149+
150+
void WatchFaceRailway::HideOverlay() {
151+
if (overlayDismissTask != nullptr) {
152+
lv_task_del(overlayDismissTask);
153+
overlayDismissTask = nullptr;
154+
}
155+
156+
// Digital's destructor cleans all screen objects
157+
delete digitalOverlay;
158+
digitalOverlay = nullptr;
159+
160+
// Recreate analog face
161+
CreateAnalogFace();
162+
UpdateClock();
163+
}
164+
165+
void WatchFaceRailway::DismissOverlayCallback(lv_task_t* task) {
166+
auto* watchface = static_cast<WatchFaceRailway*>(task->user_data);
167+
watchface->overlayDismissTask = nullptr;
168+
watchface->HideOverlay();
169+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
34+
// Hands (linemeter, rotated via angle_offset)
35+
lv_obj_t* hourHandMeter;
36+
lv_obj_t* minuteHandMeter;
37+
38+
// Center dot
39+
lv_obj_t* centerDot;
40+
41+
// Digital overlay
42+
WatchFaceDigital* digitalOverlay;
43+
lv_task_t* overlayDismissTask;
44+
45+
AppControllers& controllers;
46+
47+
void CreateAnalogFace();
48+
void UpdateClock();
49+
void ShowOverlay();
50+
void HideOverlay();
51+
static void DismissOverlayCallback(lv_task_t* task);
52+
53+
lv_task_t* taskRefresh;
54+
};
55+
}
56+
57+
template <>
58+
struct WatchFaceTraits<WatchFace::Railway> {
59+
static constexpr WatchFace watchFace = WatchFace::Railway;
60+
static constexpr const char* name = "Railway";
61+
62+
static Screens::Screen* Create(AppControllers& controllers) {
63+
return new Screens::WatchFaceRailway(controllers);
64+
};
65+
66+
static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) {
67+
return true;
68+
}
69+
};
70+
}
71+
}

0 commit comments

Comments
 (0)