Skip to content

Commit 541ff2b

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 541ff2b

File tree

6 files changed

+247
-0
lines changed

6 files changed

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