Skip to content

Commit 0bb449e

Browse files
committed
Add Legend watch face
An elegant and very practical digital watch face. Uses only fonts already in InfiniTime and no external resources. Has the standard functionality: * shows time, date (without year) and day of week * supports 24 and 12 hour time formats * BLE, new notification and alarm clock icons * battery percentage (green when charging) * steps and heart rate * temperature and weather icon Additionally: * visual indication (amber dot) for any notification(s) * visual indication (red or blue line) when notifications are muted * battery percentage turns red when 20% or less * stopwatch and timer icons (when active) * colorful sun, snow and thunderstorm weather icons * generally uses brighter colors for more urgent things
1 parent 71d1f5b commit 0bb449e

6 files changed

Lines changed: 483 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/WatchFaceLegend.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/WatchFaceLegend.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+
Legend,
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::Legend")
3233
set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware")
3334
endif()
3435

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
#include "displayapp/screens/WatchFaceLegend.h"
2+
3+
#include <lvgl/lvgl.h>
4+
#include "displayapp/screens/BleIcon.h"
5+
#include "displayapp/screens/NotificationIcon.h"
6+
#include "displayapp/screens/Symbols.h"
7+
#include "displayapp/screens/WeatherSymbols.h"
8+
#include "components/battery/BatteryController.h"
9+
#include "components/ble/BleController.h"
10+
#include "components/ble/NotificationManager.h"
11+
#include "components/heartrate/HeartRateController.h"
12+
#include "components/motion/MotionController.h"
13+
#include "components/settings/Settings.h"
14+
15+
using namespace Pinetime::Applications::Screens;
16+
17+
WatchFaceLegend::WatchFaceLegend(Controllers::DateTime& dateTimeController,
18+
const Controllers::Battery& batteryController,
19+
const Controllers::Ble& bleController,
20+
const Controllers::AlarmController& alarmController,
21+
Controllers::Timer& timerController,
22+
Controllers::StopWatchController& stopWatchController,
23+
Controllers::NotificationManager& notificationManager,
24+
Controllers::Settings& settingsController,
25+
Controllers::HeartRateController& heartRateController,
26+
Controllers::MotionController& motionController,
27+
Controllers::SimpleWeatherService& weatherService)
28+
: dateTimeController {dateTimeController},
29+
batteryController {batteryController},
30+
bleController {bleController},
31+
alarmController {alarmController},
32+
timerController {timerController},
33+
stopWatchController {stopWatchController},
34+
notificationManager {notificationManager},
35+
settingsController {settingsController},
36+
heartRateController {heartRateController},
37+
motionController {motionController},
38+
weatherService {weatherService} {
39+
40+
// ---- Top status strip -------------------------------------------------
41+
// Any notification indicator (top left). Hidden when notifications are empty.
42+
notificationAnyDot = lv_obj_create(lv_scr_act(), nullptr);
43+
lv_obj_set_size(notificationAnyDot, 7, 7);
44+
lv_obj_set_style_local_bg_color(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, colorAnyNotification);
45+
lv_obj_set_style_local_radius(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
46+
lv_obj_set_style_local_border_width(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0);
47+
lv_obj_align(notificationAnyDot, nullptr, LV_ALIGN_IN_TOP_LEFT, 18, 16);
48+
lv_obj_set_hidden(notificationAnyDot, true); // hidden by default
49+
50+
// Standard new notification indicator (used when GetNotificationStatus() is Off or Sleep).
51+
notificationNewIcon = lv_label_create(lv_scr_act(), nullptr);
52+
lv_obj_set_style_local_text_color(notificationNewIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorRed);
53+
lv_label_set_text_static(notificationNewIcon, "");
54+
lv_obj_align(notificationNewIcon, notificationAnyDot, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
55+
lv_obj_set_auto_realign(notificationNewIcon, true);
56+
57+
// Services icons. Empty string when nothing is active, can contain multiple icons.
58+
servicesIcons = lv_label_create(lv_scr_act(), nullptr);
59+
lv_obj_set_style_local_text_color(servicesIcons, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
60+
lv_label_set_text_static(servicesIcons, "");
61+
lv_obj_align(servicesIcons, notificationNewIcon, LV_ALIGN_OUT_RIGHT_MID, 12, 1);
62+
lv_obj_set_auto_realign(servicesIcons, true);
63+
64+
// Numeric battery (top right). Right-anchored with auto realign so the
65+
// invalidated region stays pinned to the corner when the width changes
66+
// (e.g. 100% -> 99%).
67+
batteryValue = lv_label_create(lv_scr_act(), nullptr);
68+
lv_obj_set_style_local_text_color(batteryValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
69+
lv_label_set_text_static(batteryValue, "--%");
70+
lv_obj_align(batteryValue, nullptr, LV_ALIGN_IN_TOP_RIGHT, -5, 10);
71+
lv_obj_set_auto_realign(batteryValue, true);
72+
73+
// BLE icon, left of the battery percentage.
74+
bleIcon = lv_label_create(lv_scr_act(), nullptr);
75+
lv_obj_set_style_local_text_color(bleIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorBlue);
76+
lv_label_set_text_static(bleIcon, "");
77+
lv_obj_align(bleIcon, batteryValue, LV_ALIGN_OUT_LEFT_MID, -10, 0);
78+
lv_obj_set_auto_realign(bleIcon, true);
79+
80+
// Upper divider - displayed (red or blue) when GetNotificationStatus() is Off or Sleep
81+
upperDivider = lv_line_create(lv_scr_act(), nullptr);
82+
lv_line_set_points(upperDivider, dividerPoints, 2);
83+
lv_obj_set_style_local_line_width(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 1);
84+
lv_obj_align(upperDivider, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 45);
85+
lv_obj_set_hidden(upperDivider, true); // hidden by default
86+
87+
// ---- Center block ------------------------------------------------------
88+
labelDate = lv_label_create(lv_scr_act(), nullptr);
89+
lv_obj_set_style_local_text_color(labelDate, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
90+
lv_label_set_text_static(labelDate, "");
91+
lv_obj_align(labelDate, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 64);
92+
93+
labelTime = lv_label_create(lv_scr_act(), nullptr);
94+
lv_obj_set_style_local_text_font(labelTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_extrabold_compressed);
95+
lv_obj_set_style_local_text_color(labelTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorForeground);
96+
lv_label_set_text_static(labelTime, "");
97+
lv_obj_align(labelTime, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 94);
98+
99+
labelAmPm = lv_label_create(lv_scr_act(), nullptr);
100+
lv_obj_set_style_local_text_color(labelAmPm, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorForeground);
101+
lv_label_set_text_static(labelAmPm, "");
102+
lv_obj_align(labelAmPm, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 64);
103+
lv_obj_set_auto_realign(labelAmPm, true);
104+
105+
divider = lv_line_create(lv_scr_act(), nullptr);
106+
lv_line_set_points(divider, dividerPoints, 2);
107+
lv_obj_set_style_local_line_color(divider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorDivider);
108+
lv_obj_set_style_local_line_width(divider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 1);
109+
lv_obj_align(divider, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 177);
110+
111+
// ---- Bottom row --------------------------------------------------
112+
stepValue = lv_label_create(lv_scr_act(), nullptr);
113+
lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
114+
lv_label_set_text_static(stepValue, "0");
115+
lv_obj_align(stepValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 186);
116+
117+
stepCaption = lv_label_create(lv_scr_act(), nullptr);
118+
lv_obj_set_style_local_text_color(stepCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption);
119+
lv_label_set_text_static(stepCaption, "steps");
120+
lv_obj_align(stepCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 210);
121+
122+
heartbeatValue = lv_label_create(lv_scr_act(), nullptr);
123+
lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
124+
lv_label_set_text_static(heartbeatValue, "--");
125+
lv_obj_align(heartbeatValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 100, 186);
126+
127+
heartbeatCaption = lv_label_create(lv_scr_act(), nullptr);
128+
lv_obj_set_style_local_text_color(heartbeatCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption);
129+
lv_label_set_text_static(heartbeatCaption, "BPM");
130+
lv_obj_align(heartbeatCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 100, 210);
131+
132+
weatherValue = lv_label_create(lv_scr_act(), nullptr);
133+
lv_obj_set_style_local_text_color(weatherValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
134+
lv_label_set_text_static(weatherValue, "--");
135+
lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 186);
136+
137+
weatherCaption = lv_label_create(lv_scr_act(), nullptr);
138+
lv_obj_set_style_local_text_color(weatherCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption);
139+
lv_label_set_text_static(weatherCaption, "\xC2\xB0");
140+
lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 210);
141+
142+
weatherIcon = lv_label_create(lv_scr_act(), nullptr);
143+
lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary);
144+
lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons);
145+
lv_label_set_text_static(weatherIcon, "");
146+
lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12);
147+
lv_obj_set_auto_realign(weatherIcon, true);
148+
149+
taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
150+
Refresh();
151+
}
152+
153+
WatchFaceLegend::~WatchFaceLegend() {
154+
lv_task_del(taskRefresh);
155+
lv_obj_clean(lv_scr_act());
156+
}
157+
158+
void WatchFaceLegend::Refresh() {
159+
// ---- Notification indicators ----
160+
notificationState = notificationManager.AreNewNotificationsAvailable();
161+
notificationEmpty = notificationManager.IsEmpty();
162+
if (notificationState.IsUpdated() || notificationEmpty.IsUpdated()) {
163+
bool state = notificationState.Get();
164+
bool empty = notificationEmpty.Get();
165+
lv_obj_set_hidden(notificationAnyDot, empty && !state);
166+
lv_label_set_text_static(notificationNewIcon, NotificationIcon::GetIcon(state));
167+
lv_obj_align(servicesIcons, state ? notificationNewIcon : notificationAnyDot, LV_ALIGN_OUT_RIGHT_MID, empty && !state ? -18 : 12, 1);
168+
}
169+
170+
// ---- Services (Alarm, Timer, StopWatch) ----
171+
alarmEnabled = alarmController.IsEnabled();
172+
timerRunning = timerController.IsRunning();
173+
stopWatchRunning = stopWatchController.IsRunning();
174+
if (alarmEnabled.IsUpdated() || timerRunning.IsUpdated() || stopWatchRunning.IsUpdated()) {
175+
lv_label_set_text_fmt(servicesIcons,
176+
"%s%s%s%s%s",
177+
stopWatchRunning.Get() ? Symbols::stopWatch : "",
178+
stopWatchRunning.Get() ? " " : "",
179+
alarmEnabled.Get() ? Symbols::bell : "",
180+
alarmEnabled.Get() ? " " : "",
181+
timerRunning.Get() ? Symbols::hourGlass : "");
182+
}
183+
184+
// ---- BLE ----
185+
bleState = bleController.IsConnected();
186+
bleRadioEnabled = bleController.IsRadioEnabled();
187+
if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) {
188+
lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get()));
189+
}
190+
191+
// ---- Battery ----
192+
batteryPercentRemaining = batteryController.PercentRemaining();
193+
powerPresent = batteryController.IsPowerPresent();
194+
if (batteryPercentRemaining.IsUpdated() || powerPresent.IsUpdated()) {
195+
uint8_t remaining = batteryPercentRemaining.Get();
196+
lv_label_set_text_fmt(batteryValue, "%d%%", remaining);
197+
lv_obj_set_style_local_text_color(batteryValue,
198+
LV_LABEL_PART_MAIN,
199+
LV_STATE_DEFAULT,
200+
powerPresent.Get() ? colorCharging
201+
: remaining > 20 ? colorSecondary
202+
: colorRed);
203+
lv_obj_align(bleIcon, batteryValue, LV_ALIGN_OUT_LEFT_MID, -10, 0);
204+
}
205+
206+
// ---- Upper divider, based on notification config ----
207+
notificationConfig = settingsController.GetNotificationStatus();
208+
if (notificationConfig.IsUpdated()) {
209+
switch (notificationConfig.Get()) {
210+
case Controllers::Settings::Notification::Off:
211+
lv_obj_set_style_local_line_color(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorRed);
212+
lv_obj_set_hidden(upperDivider, false);
213+
break;
214+
case Controllers::Settings::Notification::Sleep:
215+
lv_obj_set_style_local_line_color(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorBlue);
216+
lv_obj_set_hidden(upperDivider, false);
217+
break;
218+
default:
219+
lv_obj_set_hidden(upperDivider, true);
220+
break;
221+
}
222+
}
223+
224+
// ---- Time and date ----
225+
currentDateTime = std::chrono::time_point_cast<std::chrono::minutes>(dateTimeController.CurrentDateTime());
226+
if (currentDateTime.IsUpdated()) {
227+
uint8_t hour = dateTimeController.Hours();
228+
uint8_t minute = dateTimeController.Minutes();
229+
230+
if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) {
231+
const char* ampm = "AM";
232+
if (hour == 0) {
233+
hour = 12;
234+
} else if (hour == 12) {
235+
ampm = "PM";
236+
} else if (hour > 12) {
237+
hour = hour - 12;
238+
ampm = "PM";
239+
}
240+
lv_label_set_text_static(labelAmPm, ampm);
241+
} else {
242+
lv_label_set_text_static(labelAmPm, "");
243+
}
244+
lv_label_set_text_fmt(labelTime, "%02d:%02d", hour, minute);
245+
246+
currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get());
247+
if (currentDate.IsUpdated()) {
248+
lv_label_set_text_fmt(labelDate,
249+
"%s %02d %s",
250+
dateTimeController.DayOfWeekShortToString(),
251+
dateTimeController.Day(),
252+
dateTimeController.MonthShortToString());
253+
}
254+
}
255+
256+
// ---- Steps ----
257+
stepCount = motionController.NbSteps();
258+
if (stepCount.IsUpdated()) {
259+
lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get());
260+
}
261+
262+
// ---- Heart rate ----
263+
heartbeat = heartRateController.HeartRate();
264+
heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped;
265+
if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) {
266+
if (heartbeatRunning.Get()) {
267+
lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get());
268+
} else {
269+
lv_label_set_text_static(heartbeatValue, "--");
270+
}
271+
}
272+
273+
// ---- Weather ----
274+
currentWeather = weatherService.Current();
275+
if (currentWeather.IsUpdated()) {
276+
if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) {
277+
lv_label_set_text_static(weatherCaption,
278+
"\xC2\xB0"
279+
"F");
280+
} else {
281+
lv_label_set_text_static(weatherCaption,
282+
"\xC2\xB0"
283+
"C");
284+
}
285+
286+
auto optCurrentWeather = currentWeather.Get();
287+
if (optCurrentWeather) {
288+
int16_t temp;
289+
if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) {
290+
temp = optCurrentWeather->temperature.Fahrenheit();
291+
} else {
292+
temp = optCurrentWeather->temperature.Celsius();
293+
}
294+
if (temp > -100) {
295+
lv_label_set_text_fmt(weatherValue, "%d", temp);
296+
} else {
297+
lv_label_set_text_static(weatherValue, "--");
298+
}
299+
300+
lv_color_t color;
301+
switch (optCurrentWeather->iconId) {
302+
case Controllers::SimpleWeatherService::Icons::Sun:
303+
color = colorWeatherYellow;
304+
break;
305+
case Controllers::SimpleWeatherService::Icons::Snow:
306+
case Controllers::SimpleWeatherService::Icons::Thunderstorm:
307+
color = colorWeatherLightBlue;
308+
break;
309+
default:
310+
color = colorWeatherDefault;
311+
break;
312+
}
313+
lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color);
314+
lv_label_set_text_static(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId, weatherService.IsNight()));
315+
316+
if (!weatherDisplayed) { // adjust the layout when necessary
317+
lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 200, 186);
318+
lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 200, 210);
319+
lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12);
320+
weatherDisplayed = true;
321+
}
322+
} else {
323+
lv_label_set_text_static(weatherValue, "--");
324+
lv_label_set_text_static(weatherIcon, "");
325+
326+
if (weatherDisplayed) { // adjust the layout when necessary
327+
lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 186);
328+
lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 210);
329+
lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12);
330+
weatherDisplayed = false;
331+
}
332+
}
333+
}
334+
}

0 commit comments

Comments
 (0)