Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ list(APPEND SOURCE_FILES
displayapp/screens/Screen.cpp
displayapp/screens/Tile.cpp
displayapp/screens/InfiniPaint.cpp
displayapp/screens/Paddle.cpp
# displayapp/screens/Paddle.cpp
displayapp/screens/StopWatch.cpp
displayapp/screens/BatteryIcon.cpp
displayapp/screens/BleIcon.cpp
Expand All @@ -384,7 +384,7 @@ list(APPEND SOURCE_FILES
displayapp/screens/FirmwareValidation.cpp
displayapp/screens/ApplicationList.cpp
displayapp/screens/Notifications.cpp
displayapp/screens/Twos.cpp
# displayapp/screens/Twos.cpp
displayapp/screens/HeartRate.cpp
displayapp/screens/FlashLight.cpp
displayapp/screens/List.cpp
Expand Down Expand Up @@ -424,7 +424,7 @@ list(APPEND SOURCE_FILES
## Watch faces
displayapp/screens/WatchFaceAnalog.cpp
displayapp/screens/WatchFaceDigital.cpp
displayapp/screens/WatchFaceInfineat.cpp
# displayapp/screens/WatchFaceInfineat.cpp
displayapp/screens/WatchFaceTerminal.cpp
displayapp/screens/WatchFacePineTimeStyle.cpp
displayapp/screens/WatchFaceCasioStyleG7710.cpp
Expand Down
4 changes: 2 additions & 2 deletions src/displayapp/DisplayApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
#include "displayapp/screens/FirmwareUpdate.h"
#include "displayapp/screens/FirmwareValidation.h"
#include "displayapp/screens/InfiniPaint.h"
#include "displayapp/screens/Paddle.h"
// #include "displayapp/screens/Paddle.h"
#include "displayapp/screens/StopWatch.h"
#include "displayapp/screens/Metronome.h"
#include "displayapp/screens/Music.h"
#include "displayapp/screens/Navigation.h"
#include "displayapp/screens/Notifications.h"
#include "displayapp/screens/SystemInfo.h"
#include "displayapp/screens/Tile.h"
#include "displayapp/screens/Twos.h"
// #include "displayapp/screens/Twos.h"
#include "displayapp/screens/FlashLight.h"
#include "displayapp/screens/BatteryInfo.h"
#include "displayapp/screens/Steps.h"
Expand Down
4 changes: 2 additions & 2 deletions src/displayapp/apps/Apps.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ namespace Pinetime {
BatteryInfo,
Music,
Paint,
Paddle,
Twos,
// Paddle,
// Twos,
HeartRate,
Navigation,
StopWatch,
Expand Down
257 changes: 165 additions & 92 deletions src/displayapp/screens/WatchFaceDigital.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

#include <lvgl/lvgl.h>
#include <cstdio>

#include "displayapp/screens/BatteryIcon.h"
#include "displayapp/screens/BleIcon.h"
#include "displayapp/screens/NotificationIcon.h"
#include "displayapp/screens/Symbols.h"
#include "displayapp/screens/WeatherSymbols.h"
Expand All @@ -11,7 +12,6 @@
#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;
Expand All @@ -32,59 +32,78 @@ WatchFaceDigital::WatchFaceDigital(Controllers::DateTime& dateTimeController,
heartRateController {heartRateController},
motionController {motionController},
weatherService {weatherService},
statusIcons(batteryController, bleController, alarmController) {
statusIcons(batteryController, bleController, alarmController),
basePraxiomAge(53), // Demo age - will be replaced when phone app connects
lastSyncTime(0) {

statusIcons.Create();
// Create Praxiom brand gradient background (Orange/Amber to Teal/Cyan)
lv_obj_t* background_gradient = lv_obj_create(lv_scr_act(), nullptr);
lv_obj_set_size(background_gradient, 240, 240);
lv_obj_set_pos(background_gradient, 0, 0);
lv_obj_set_style_local_radius(background_gradient, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0);
// Praxiom brand colors: Orange/Amber (#CC6600) to Teal (#008B8B)
lv_obj_set_style_local_bg_color(background_gradient, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCC6600));
lv_obj_set_style_local_bg_grad_color(background_gradient, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x008B8B));
lv_obj_set_style_local_bg_grad_dir(background_gradient, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_GRAD_DIR_VER);
lv_obj_set_style_local_border_width(background_gradient, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0);

notificationIcon = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_LIME);
lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(false));
lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0);

weatherIcon = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999));
lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons);
lv_label_set_text(weatherIcon, "");
lv_obj_align(weatherIcon, nullptr, LV_ALIGN_IN_TOP_MID, -20, 50);
lv_obj_set_auto_realign(weatherIcon, true);
statusIcons.Create();
lv_obj_align(statusIcons.GetObject(), lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, -8, 0);

temperature = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999));
lv_label_set_text(temperature, "");
lv_obj_align(temperature, nullptr, LV_ALIGN_IN_TOP_MID, 20, 50);
// Create and position Praxiom Age label (text) - BLACK for contrast
labelPraxiomAge = lv_label_create(lv_scr_act(), nullptr);
lv_label_set_text_static(labelPraxiomAge, "Praxiom Age");
lv_obj_set_style_local_text_font(labelPraxiomAge, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
lv_obj_set_style_local_text_color(labelPraxiomAge, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_obj_align(labelPraxiomAge, lv_scr_act(), LV_ALIGN_CENTER, 0, -80);

label_date = lv_label_create(lv_scr_act(), nullptr);
lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_CENTER, 0, 60);
lv_obj_set_style_local_text_color(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999));
// Create Praxiom Age number - BOLD large digits - WHITE (neutral color)
labelPraxiomAgeNumber = lv_label_create(lv_scr_act(), nullptr);
lv_label_set_text_static(labelPraxiomAgeNumber, "53"); // Initial demo value
lv_obj_set_style_local_text_font(labelPraxiomAgeNumber, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_extrabold_compressed); // BOLD font
lv_obj_set_style_local_text_color(labelPraxiomAgeNumber, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xFFFFFF)); // Start with white
lv_obj_align(labelPraxiomAgeNumber, lv_scr_act(), LV_ALIGN_CENTER, 0, -10);

// Time label - BLACK
label_time = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_font(label_time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_extrabold_compressed);

lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, 0, 0);
lv_obj_set_style_local_text_font(label_time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42);
lv_obj_set_style_local_text_color(label_time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_label_set_text_static(label_time, "00:00");
lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 55);

label_time_ampm = lv_label_create(lv_scr_act(), nullptr);
lv_label_set_text_static(label_time_ampm, "");
lv_obj_align(label_time_ampm, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -30, -55);
// Date label - BLACK
label_date = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_font(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
lv_obj_set_style_local_text_color(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_CENTER, 0, 90);

// Heart rate icon and label - BLACK
heartbeatIcon = lv_label_create(lv_scr_act(), nullptr);
lv_label_set_text_static(heartbeatIcon, Symbols::heartBeat);
lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B));
lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0);

heartbeatValue = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B));
lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_label_set_text_static(heartbeatValue, "");
lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0);

// Step icon and label - BLACK
stepIcon = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(stepIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_label_set_text_static(stepIcon, Symbols::shoe);
lv_obj_align(stepIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0);

stepValue = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x00FFE7));
lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_label_set_text_static(stepValue, "0");
lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0);
lv_obj_align(stepValue, stepIcon, LV_ALIGN_OUT_LEFT_MID, -5, 0);

stepIcon = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(stepIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x00FFE7));
lv_label_set_text_static(stepIcon, Symbols::shoe);
lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0);
// Notification icon - BLACK
notificationIcon = lv_label_create(lv_scr_act(), nullptr);
lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x000000));
lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(false));
lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0);

taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
Refresh();
Expand All @@ -95,10 +114,94 @@ WatchFaceDigital::~WatchFaceDigital() {
lv_obj_clean(lv_scr_act());
}

// Update base Praxiom Age from phone app (called via BLE)
void WatchFaceDigital::UpdateBasePraxiomAge(int age) {
basePraxiomAge = age;
lastSyncTime = dateTimeController.CurrentDateTime().time_since_epoch().count();
}

// Calculate real-time adjustment based on watch sensors
float WatchFaceDigital::CalculateRealtimeAdjustment() {
float adjustment = 0.0f;

// Get current sensor data
uint8_t currentHeartRate = heartRateController.HeartRate();
uint32_t currentSteps = motionController.NbSteps();

// Heart rate deviation adjustment (-1 to +2 years based on HR)
if (currentHeartRate > 0) {
if (currentHeartRate < 50 || currentHeartRate > 90) {
// Poor resting HR
adjustment += 1.5f;
} else if (currentHeartRate >= 50 && currentHeartRate <= 60) {
// Excellent resting HR
adjustment -= 0.5f;
} else if (currentHeartRate >= 61 && currentHeartRate <= 70) {
// Good resting HR
adjustment -= 0.2f;
} else if (currentHeartRate >= 71 && currentHeartRate <= 80) {
// Fair resting HR
adjustment += 0.3f;
} else {
// Suboptimal resting HR
adjustment += 0.8f;
}
}

// Daily activity adjustment (-1 to +1 year based on steps)
if (currentSteps >= 10000) {
adjustment -= 0.8f; // Excellent activity
} else if (currentSteps >= 8000) {
adjustment -= 0.3f; // Good activity
} else if (currentSteps >= 5000) {
adjustment += 0.2f; // Moderate activity
} else if (currentSteps >= 3000) {
adjustment += 0.5f; // Low activity
} else {
adjustment += 1.0f; // Sedentary
}

// Cap adjustment to reasonable range
if (adjustment < -2.0f) adjustment = -2.0f;
if (adjustment > 3.0f) adjustment = 3.0f;

return adjustment;
}

// Calculate final Praxiom Age
int WatchFaceDigital::GetCurrentPraxiomAge() {
// Demo mode: basePraxiomAge = 53 until phone app connects and updates it
// Real mode: basePraxiomAge updated by phone app via UpdateBasePraxiomAge()

float adjustment = CalculateRealtimeAdjustment();
int finalAge = basePraxiomAge + (int)adjustment;

// Ensure reasonable bounds (18-120)
if (finalAge < 18) finalAge = 18;
if (finalAge > 120) finalAge = 120;

return finalAge;
}

// Get color based on Praxiom Age difference from base age
lv_color_t WatchFaceDigital::GetPraxiomAgeColor(int currentAge, int baseAge) {
int difference = currentAge - baseAge;

if (difference < -2) {
// More than 2 years younger - GREEN (excellent health)
return lv_color_hex(0x00FF00);
} else if (difference > 2) {
// More than 2 years older - RED (poor health)
return lv_color_hex(0xFF0000);
} else {
// Within ±2 years - WHITE (neutral/good health)
return lv_color_hex(0xFFFFFF);
}
}

void WatchFaceDigital::Refresh() {
statusIcons.Update();

notificationState = notificationManager.AreNewNotificationsAvailable();
if (notificationState.IsUpdated()) {
lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(notificationState.Get()));
}
Expand All @@ -109,43 +212,18 @@ void WatchFaceDigital::Refresh() {
uint8_t hour = dateTimeController.Hours();
uint8_t minute = dateTimeController.Minutes();

if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) {
char ampmChar[3] = "AM";
if (hour == 0) {
hour = 12;
} else if (hour == 12) {
ampmChar[0] = 'P';
} else if (hour > 12) {
hour = hour - 12;
ampmChar[0] = 'P';
}
lv_label_set_text(label_time_ampm, ampmChar);
lv_label_set_text_fmt(label_time, "%2d:%02d", hour, minute);
lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, 0, 0);
} else {
lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute);
lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
}
// Update time
lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute);

currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get());
if (currentDate.IsUpdated()) {
uint16_t year = dateTimeController.Year();
uint8_t day = dateTimeController.Day();
if (settingsController.GetClockType() == Controllers::Settings::ClockType::H24) {
lv_label_set_text_fmt(label_date,
"%s %d %s %d",
dateTimeController.DayOfWeekShortToString(),
day,
dateTimeController.MonthShortToString(),
year);
} else {
lv_label_set_text_fmt(label_date,
"%s %s %d %d",
dateTimeController.DayOfWeekShortToString(),
dateTimeController.MonthShortToString(),
day,
year);
}
lv_label_set_text_fmt(label_date,
"%s %d %d",
dateTimeController.DayOfWeekShortToString(),
day,
year);
lv_obj_realign(label_date);
}
}
Expand All @@ -154,41 +232,36 @@ void WatchFaceDigital::Refresh() {
heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped;
if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) {
if (heartbeatRunning.Get()) {
lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B));
lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get());
} else {
lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x1B1B1B));
lv_label_set_text_static(heartbeatValue, "");
}

lv_obj_realign(heartbeatIcon);
lv_obj_realign(heartbeatValue);
}

stepCount = motionController.NbSteps();
if (stepCount.IsUpdated()) {
lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get());
lv_obj_realign(stepValue);
lv_obj_realign(stepIcon);
}

currentWeather = weatherService.Current();
if (currentWeather.IsUpdated()) {
auto optCurrentWeather = currentWeather.Get();
if (optCurrentWeather) {
int16_t temp = optCurrentWeather->temperature.Celsius();
char tempUnit = 'C';
if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) {
temp = optCurrentWeather->temperature.Fahrenheit();
tempUnit = 'F';
}
lv_label_set_text_fmt(temperature, "%d°%c", temp, tempUnit);
lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId));
} else {
lv_label_set_text_static(temperature, "");
lv_label_set_text(weatherIcon, "");
}
lv_obj_realign(temperature);
lv_obj_realign(weatherIcon);
// Update Praxiom Age every minute with dynamic color
static uint8_t lastMinute = 0;
uint8_t currentMinute = dateTimeController.Minutes();

if (currentMinute != lastMinute) {
int praxiomAge = GetCurrentPraxiomAge();

// Update the number
lv_label_set_text_fmt(labelPraxiomAgeNumber, "%d", praxiomAge);

// Update the color based on difference from base age
lv_color_t ageColor = GetPraxiomAgeColor(praxiomAge, basePraxiomAge);
lv_obj_set_style_local_text_color(labelPraxiomAgeNumber, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, ageColor);

lv_obj_realign(labelPraxiomAgeNumber);
lastMinute = currentMinute;

// TODO: Send updated Praxiom Age back to phone via BLE every 10 minutes
}
}
Loading