Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2024-12-12 16:43:03 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View File

@@ -0,0 +1,661 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "kickstart.h"
#include "applib/app.h"
#include "applib/graphics/text.h"
#include "applib/tick_timer_service.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/ui.h"
#include "apps/system_apps/timeline/text_node.h"
#include "kernel/pbl_malloc.h"
#include "applib/pbl_std/pbl_std.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/clock.h"
#include "services/normal/activity/health_util.h"
#include "util/size.h"
#include "util/string.h"
#include "util/time/time.h"
#include "util/trig.h"
#include <string.h>
#define ROBERT_SCREEN_RES (PBL_DISPLAY_WIDTH == 200 && PBL_DISPLAY_HEIGHT == 228)
#define SNOWY_SCREEN_RES (PBL_DISPLAY_WIDTH == 144 && PBL_DISPLAY_HEIGHT == 168)
#define SPALDING_SCREEN_RES (PBL_DISPLAY_WIDTH == 180 && PBL_DISPLAY_HEIGHT == 180)
////////////////////////////////////////////////////////////////////////////////////////////////////
// UI Utils
#if UNITTEST
static int16_t s_unobstructed_area_height = 0;
T_STATIC void prv_set_unobstructed_area_height(int16_t height) {
s_unobstructed_area_height = height;
}
#endif
#define MULT_X(a, b) (b ? (1000 * a / b) : 0)
#define DIV_X(a) (a / 1000)
static GPoint prv_steps_to_point(int32_t cur, int32_t total, GRect frame) {
#if PBL_RECT
/* e 0 b
* ---------
* | |
* | |
* | |
* | |
* | |
* ---------
* d c
*/
const int32_t top_right = frame.size.w / 2;
const int32_t bot_right = frame.size.h + top_right;
const int32_t bot_left = frame.size.w + bot_right;
const int32_t top_left = frame.size.h + bot_left;
const int32_t rect_perimeter = top_left + top_right;
// limits calculated from length along perimeter starting from '0'
const int32_t limit_b = total * top_right / rect_perimeter;
const int32_t limit_c = total * bot_right / rect_perimeter;
const int32_t limit_d = total * bot_left / rect_perimeter;
const int32_t limit_e = total * top_left / rect_perimeter;
if (cur <= limit_b) {
// zone 0 - b
return GPoint(frame.origin.x + DIV_X(frame.size.w * (500 + (MULT_X(cur, limit_b) / 2))),
frame.origin.y);
} else if (cur <= limit_c) {
// zone b - c
return GPoint(frame.origin.x + frame.size.w,
frame.origin.y +
DIV_X(frame.size.h * MULT_X((cur - limit_b), (limit_c - limit_b))));
} else if (cur <= limit_d) {
// zone c - d
return GPoint(frame.origin.x +
DIV_X(frame.size.w * (1000 - MULT_X((cur - limit_c), (limit_d - limit_c)))),
frame.origin.y + frame.size.h);
} else if (cur <= limit_e) {
// zone d - e
return GPoint(frame.origin.x,
frame.origin.y +
DIV_X(frame.size.h * (1000 - MULT_X((cur - limit_d), (limit_e - limit_d)))));
} else {
// zone e - 0
return GPoint(frame.origin.x +
DIV_X(frame.size.w / 2 * MULT_X((cur - limit_e), (total - limit_e))),
frame.origin.y);
}
#elif PBL_ROUND
// Simply a calculated point on the circumference
const int32_t angle = DIV_X(360 * MULT_X(cur, total));
return gpoint_from_polar(frame, GOvalScaleModeFitCircle, DEG_TO_TRIGANGLE(angle));
#endif
}
#if PBL_RECT
static GPoint prv_inset_point(GRect *frame, GPoint outer_point, int32_t inset_amount) {
// Insets the given point by the specified amount
return (GPoint) {
.x = MAX(inset_amount - 1, MIN(outer_point.x, frame->size.w - inset_amount)),
.y = MAX(inset_amount - 1, MIN(outer_point.y, frame->size.h - inset_amount))
};
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////
// UI Drawing
static void prv_draw_outer_ring(GContext *ctx, int32_t current, int32_t total,
int32_t fill_thickness, GRect frame, GColor color) {
graphics_context_set_fill_color(ctx, color);
const GRect outer_bounds = grect_inset(frame, GEdgeInsets(-1));
#if PBL_RECT
GPoint start_outer_point = prv_steps_to_point(0, total, outer_bounds);
GPoint start_inner_point = prv_inset_point(&frame, start_outer_point, fill_thickness);
GPoint end_outer_point = prv_steps_to_point(current, total, outer_bounds);
GPoint end_inner_point = prv_inset_point(&frame, end_outer_point, fill_thickness);
#if PBL_BW
// Make sure we draw something if we have any steps
if ((start_outer_point.y == end_outer_point.y) && (end_outer_point.x > start_outer_point.x) &&
(end_outer_point.x - start_outer_point.x < 3)) {
end_outer_point.x = start_outer_point.x + 3;
end_inner_point.x = start_inner_point.x + 3;
}
#endif
const int32_t max_points = 20;
GPath path = (GPath) {
.points = app_zalloc_check(sizeof(GPoint) * max_points),
.num_points = 0,
};
const int32_t top_right = frame.size.w / 2;
const int32_t bot_right = frame.size.h + top_right;
const int32_t bot_left = frame.size.w + bot_right;
const int32_t top_left = frame.size.h + bot_left;
const int32_t rect_perimeter = top_left + top_right;
const int32_t corners[] = {0,
total * top_right / rect_perimeter,
total * bot_right / rect_perimeter,
total * bot_left / rect_perimeter,
total * top_left / rect_perimeter,
total};
// start the path with start_outer_point
path.points[path.num_points++] = start_outer_point;
// loop through and add all the corners b/w start and end
for (uint16_t i = 0; i < ARRAY_LENGTH(corners); i++) {
if (corners[i] > 0 && corners[i] < current) {
path.points[path.num_points++] = prv_steps_to_point(corners[i], total, outer_bounds);
}
}
// add end outer and inner points
path.points[path.num_points++] = end_outer_point;
path.points[path.num_points++] = end_inner_point;
// loop though backwards and add all the corners b/w end and start
for (int i = ARRAY_LENGTH(corners) - 1; i >= 0; i--) {
if (corners[i] > 0 && corners[i] < current) {
path.points[path.num_points++] = prv_inset_point(
&frame, prv_steps_to_point(corners[i], total, outer_bounds), fill_thickness);
}
}
// add start_inner_point
path.points[path.num_points++] = start_inner_point;
gpath_draw_filled(ctx, &path);
#if PBL_COLOR
graphics_context_set_stroke_color(ctx, color);
gpath_draw_outline(ctx, &path);
#else
graphics_context_set_stroke_color(ctx, GColorWhite);
GRect inner_bounds = grect_inset(outer_bounds, GEdgeInsets(fill_thickness));
graphics_draw_rect(ctx, &inner_bounds);
inner_bounds = grect_inset(inner_bounds, GEdgeInsets(-1));
graphics_draw_rect(ctx, &inner_bounds);
#endif
app_free(path.points);
#elif PBL_ROUND
const int32_t degree = total ? (360 * current / total) : 0;
const int32_t to_angle = DEG_TO_TRIGANGLE(degree);
graphics_fill_radial(ctx, outer_bounds, GOvalScaleModeFitCircle, fill_thickness, 0, to_angle);
#endif
}
#if PBL_ROUND
static void prv_draw_outer_dots(GContext *ctx, GRect bounds) {
const GRect inset_bounds = grect_inset(bounds, GEdgeInsets(6));
// outer dots placed along inside circumference
const int num_dots = 12;
for (int i = 0; i < num_dots; i++) {
GPoint pos = gpoint_from_polar(inset_bounds, GOvalScaleModeFitCircle,
DEG_TO_TRIGANGLE(i * 360 / num_dots));
const int dot_radius = 2;
graphics_context_set_fill_color(ctx, GColorDarkGray);
graphics_fill_circle(ctx, pos, dot_radius);
}
}
#endif
static void prv_draw_goal_line(GContext *ctx, int32_t current_progress, int32_t total_progress,
int32_t line_length, int32_t line_width, GRect frame, GColor color) {
const GPoint line_outer_point = prv_steps_to_point(current_progress, total_progress, frame);
#if PBL_RECT
const GPoint line_inner_point = prv_inset_point(&frame, line_outer_point, line_length);
#elif PBL_ROUND
const GRect inner_bounds = grect_inset(frame, GEdgeInsets(line_length));
const GPoint line_inner_point = prv_steps_to_point(current_progress,
total_progress, inner_bounds);
#endif
graphics_context_set_stroke_color(ctx, color);
graphics_context_set_stroke_width(ctx, line_width);
graphics_draw_line(ctx, line_inner_point, line_outer_point);
}
#if ROBERT_SCREEN_RES
static void prv_draw_seperator(GContext *ctx, GRect bounds, GColor color) {
bounds.origin.y += 111; // top offset
GPoint p1 = bounds.origin;
GPoint p2 = p1;
p2.x += bounds.size.w;
graphics_context_set_stroke_color(ctx, color);
graphics_context_set_stroke_width(ctx, 1);
graphics_draw_line(ctx, p1, p2);
}
#endif
static void prv_draw_steps_and_shoe(GContext *ctx, const char *steps_buffer, GFont font,
GRect bounds, GColor color, GBitmap *shoe_icon,
bool screen_is_obstructed, bool has_bpm) {
#if PBL_BW
bounds.origin.y += screen_is_obstructed ? (has_bpm ? 74 : 66) : (has_bpm ? 114 : 96);
#elif ROBERT_SCREEN_RES
bounds.origin.y += screen_is_obstructed ? 113 : 158;
#elif SNOWY_SCREEN_RES
if (screen_is_obstructed) {
bounds = grect_inset(bounds, GEdgeInsets(0, 20));
}
#endif
GRect icon_bounds = gbitmap_get_bounds(shoe_icon);
icon_bounds.origin = bounds.origin;
#if PBL_BW
icon_bounds.origin.x += 23; // icon left offset
icon_bounds.origin.y += 9; // icon top offset
#elif ROBERT_SCREEN_RES
icon_bounds.origin.y += (46 - icon_bounds.size.h); // icon top offest
#elif SNOWY_SCREEN_RES
icon_bounds.origin.x = screen_is_obstructed ? bounds.origin.x // icon_left offset
: (bounds.size.w / 2) - (icon_bounds.size.w / 2);
icon_bounds.origin.y += screen_is_obstructed ? 84 : 22; // icon top offset
#elif SPALDING_SCREEN_RES
icon_bounds.origin.x = (bounds.size.w / 2) - (icon_bounds.size.w / 2);
icon_bounds.origin.y += 27; // icon top offset
#endif
graphics_context_set_compositing_mode(ctx, GCompOpSet);
graphics_draw_bitmap_in_rect(ctx, shoe_icon, &icon_bounds);
#if PBL_BW
const GTextAlignment alignment = GTextAlignmentLeft;
bounds.origin.x += 62; // steps text left offset
#elif ROBERT_SCREEN_RES
const GTextAlignment alignment = GTextAlignmentRight;
#elif SNOWY_SCREEN_RES
const GTextAlignment alignment = screen_is_obstructed ? GTextAlignmentRight: GTextAlignmentCenter;
bounds.origin.y += screen_is_obstructed ? 65 : 108; // steps text top offset
#elif SPALDING_SCREEN_RES
const GTextAlignment alignment = GTextAlignmentCenter;
bounds.origin.y += 113; // steps text top offset
#endif
graphics_context_set_text_color(ctx, color);
graphics_draw_text(ctx, steps_buffer, font, bounds, GTextOverflowModeFill, alignment, NULL);
}
static void prv_draw_time(GContext *ctx, GFont time_font, GFont am_pm_font, GRect bounds,
bool screen_is_obstructed, bool has_bpm) {
GTextNodeHorizontal *horiz_container = graphics_text_node_create_horizontal(MAX_TEXT_NODES);
GTextNodeContainer *container = &horiz_container->container;
horiz_container->horizontal_alignment = GTextAlignmentCenter;
char time_buffer[8];
char am_pm_buffer[4];
const time_t now = rtc_get_time();
/// Current time in 24 or 12 hour
const char *time_fmt = clock_is_24h_style() ? "%R" : "%l:%M";
strftime(time_buffer, sizeof(time_buffer), time_fmt, pbl_override_localtime(&now));
health_util_create_text_node_with_text(
string_strip_leading_whitespace(time_buffer),
time_font, GColorWhite, container);
if (!clock_is_24h_style()) {
/// AM/PM for the current time
strftime(am_pm_buffer, sizeof(am_pm_buffer), "%p", pbl_override_localtime(&now));
health_util_create_text_node_with_text(
am_pm_buffer, am_pm_font, GColorWhite, container);
}
#if PBL_BW
bounds.origin.y = screen_is_obstructed ? (has_bpm ? 13 : 23) : (has_bpm ? 36 : 53);
#elif ROBERT_SCREEN_RES
bounds.origin.y = screen_is_obstructed ? -12 : 6;
#elif SNOWY_SCREEN_RES
bounds.origin.y = screen_is_obstructed ? 4 : 47;
#elif SPALDING_SCREEN_RES
bounds.origin.y = 50;
#endif
graphics_text_node_draw(&container->node, ctx, &bounds, NULL, NULL);
graphics_text_node_destroy(&container->node);
}
#if PBL_BW || ROBERT_SCREEN_RES
static void prv_draw_bpm(GContext *ctx, int32_t current_bpm, GFont font, GBitmap *heart_icon,
GRect bounds, bool screen_is_obstructed, void *i18n_owner) {
#if PBL_BW
bounds.origin.y += screen_is_obstructed ? 52 : 89;
#elif ROBERT_SCREEN_RES
bounds.origin.y += screen_is_obstructed ? 80 : 123;
#endif
GRect icon_bounds = gbitmap_get_bounds(heart_icon);
icon_bounds.origin = bounds.origin;
#if PBL_BW
icon_bounds.origin.x += 20; // icon left offset
#endif
graphics_context_set_compositing_mode(ctx, GCompOpSet);
graphics_draw_bitmap_in_rect(ctx, heart_icon, &icon_bounds);
char bpm_text[16];
snprintf(bpm_text, sizeof(bpm_text), i18n_get("%d BPM", i18n_owner), current_bpm);
#if PBL_BW
bounds.origin.x += 62; // bpm text left offset
#endif
bounds.origin.y -= PBL_IF_BW_ELSE(5, 8); // bpm text top offset
const GTextAlignment alignment = PBL_IF_BW_ELSE(GTextAlignmentLeft, GTextAlignmentRight);
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorRed, GColorWhite));
graphics_draw_text(ctx, bpm_text, font, bounds, GTextOverflowModeFill, alignment, NULL);
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////
// Update Proc
static void prv_base_layer_update_proc(Layer *layer, GContext *ctx) {
KickstartData *data = window_get_user_data(layer_get_window(layer));
GRect bounds = layer->bounds;
GRect unobstructed_bounds;
layer_get_unobstructed_bounds(layer, &unobstructed_bounds);
#if UNITTEST
unobstructed_bounds.size.h = bounds.size.h - s_unobstructed_area_height;
#endif
const bool screen_is_obstructed = (unobstructed_bounds.size.h != bounds.size.h);
bounds.size.h = unobstructed_bounds.size.h;
#if SNOWY_SCREEN_RES
const int16_t fill_thickness = screen_is_obstructed ? 10 : 11;
#elif ROBERT_SCREEN_RES
const int16_t fill_thickness = screen_is_obstructed ? 5 : 13;
#elif SPALDING_SCREEN_RES
const int16_t fill_thickness = (bounds.size.h - grect_inset(bounds, GEdgeInsets(15)).size.h) / 2;
#endif
#if PBL_COLOR
const bool has_passed_goal = (data->current_steps > data->typical_steps);
const GColor fill_color = has_passed_goal ? GColorJaegerGreen : GColorVividCerulean;
const GColor text_color = has_passed_goal ? GColorJaegerGreen : GColorVividCerulean;
#if SNOWY_SCREEN_RES
GBitmap *shoe =
has_passed_goal ? (screen_is_obstructed ? &data->shoe_green_small : &data->shoe_green)
: (screen_is_obstructed ? &data->shoe_blue_small : &data->shoe_blue);
#else
GBitmap *shoe = has_passed_goal ? &data->shoe_green : &data->shoe_blue;
#endif // SNOWY_SCREEN_RES
#else
const GColor fill_color = GColorDarkGray;
const GColor text_color = GColorWhite;
GBitmap *shoe = &data->shoe;
#endif // PBL_COLOR
#if PBL_ROUND
prv_draw_outer_dots(ctx, bounds);
#endif
// draw outer ring
prv_draw_outer_ring(ctx, data->current_steps, data->daily_steps_avg,
fill_thickness, bounds, fill_color);
const int goal_line_length = PBL_IF_COLOR_ELSE(fill_thickness + 3, 12);
const int goal_line_width = 4;
// draw yellow goal line
prv_draw_goal_line(ctx, data->typical_steps, MAX(data->daily_steps_avg, data->typical_steps),
goal_line_length, goal_line_width, bounds, GColorYellow);
const bool has_bpm = (data->current_bpm > 0);
// draw time
prv_draw_time(ctx, data->time_font, PBL_IF_COLOR_ELSE(data->am_pm_font, data->time_font),
bounds, screen_is_obstructed, has_bpm);
#if ROBERT_SCREEN_RES
bounds = grect_inset(bounds, GEdgeInsets(0, 25));
// draw deperator
if (!screen_is_obstructed) {
prv_draw_seperator(ctx, bounds, GColorWhite);
}
#endif
#if PBL_BW || ROBERT_SCREEN_RES
// draw bpm and heart
if (has_bpm) {
prv_draw_bpm(ctx, data->current_bpm, data->steps_font, &data->heart_icon, bounds,
screen_is_obstructed, data);
}
#endif
// draw steps and shoe
prv_draw_steps_and_shoe(ctx, data->steps_buffer, data->steps_font, bounds, text_color, shoe,
screen_is_obstructed, has_bpm);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Data
static void prv_update_steps_buffer(KickstartData *data) {
const int thousands = data->current_steps / 1000;
const int hundreds = data->current_steps % 1000;
if (thousands) {
/// Step count greater than 1000 with a thousands seperator
snprintf(data->steps_buffer, sizeof(data->steps_buffer), i18n_get("%d,%03d", data),
thousands, hundreds);
} else {
/// Step count less than 1000
snprintf(data->steps_buffer, sizeof(data->steps_buffer), i18n_get("%d", data),
hundreds);
}
layer_mark_dirty(&data->base_layer);
}
static void prv_update_current_steps(KickstartData *data) {
data->current_steps = health_service_sum_today(HealthMetricStepCount);
prv_update_steps_buffer(data);
}
static void prv_update_typical_steps(KickstartData *data) {
data->typical_steps = health_service_sum_averaged(HealthMetricStepCount,
time_start_of_today(),
rtc_get_time(),
HealthServiceTimeScopeWeekly);
}
static void prv_update_daily_steps_avg(KickstartData *data) {
data->daily_steps_avg = health_service_sum_averaged(HealthMetricStepCount,
time_start_of_today(),
time_start_of_today() + SECONDS_PER_DAY,
HealthServiceTimeScopeWeekly);
}
static void prv_update_hrm_bpm(KickstartData *data) {
data->current_bpm = health_service_peek_current_value(HealthMetricHeartRateBPM);
}
static void prv_normalize_data(KickstartData *data) {
// If the user's daily avg steps are very low (QA or a brand new pebble user), bump the value
// to a slightly more reasonable number.
// This fixes an integer rounding problem when the value is very small (PBL-43717)
const int min_daily_steps_avg = 100;
data->daily_steps_avg = MAX(data->daily_steps_avg, min_daily_steps_avg);
// increase daily avg 5% more than current steps if current steps is more than 95% of daily avg
if (data->current_steps >= (data->daily_steps_avg * 95 / 100)) {
data->daily_steps_avg = data->current_steps * 105 / 100;
}
}
static void prv_update_data(KickstartData *data) {
prv_update_current_steps(data);
prv_update_typical_steps(data);
prv_update_daily_steps_avg(data);
prv_update_hrm_bpm(data);
prv_normalize_data(data);
layer_mark_dirty(&data->base_layer);
}
#if UNITTEST
T_STATIC void prv_set_data(KickstartData *data, int32_t current_steps,
int32_t typical_steps, int32_t daily_steps_avg, int32_t current_bpm) {
data->current_steps = current_steps;
data->typical_steps = typical_steps;
data->daily_steps_avg = daily_steps_avg;
data->current_bpm = current_bpm;
prv_normalize_data(data);
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////
// Handlers
static void prv_health_service_events_handler(HealthEventType event, void *context) {
if (event == HealthEventMovementUpdate) {
prv_update_current_steps(context);
}
}
static void prv_tick_handler(struct tm *tick_time, TimeUnits changed) {
KickstartData *data = app_state_get_user_data();
prv_update_data(data);
}
T_STATIC void prv_window_load_handler(Window *window) {
KickstartData *data = window_get_user_data(window);
// load resources
#if PBL_BW
gbitmap_init_with_resource(&data->shoe, RESOURCE_ID_STRIDE_SHOE);
gbitmap_init_with_resource(&data->heart_icon, RESOURCE_ID_WORKOUT_APP_HEART);
data->steps_font = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
data->time_font = fonts_get_system_font(FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM);
#else
gbitmap_init_with_resource(&data->shoe_blue, RESOURCE_ID_STRIDE_SHOE_BLUE);
gbitmap_init_with_resource(&data->shoe_green, RESOURCE_ID_STRIDE_SHOE_GREEN);
#if ROBERT_SCREEN_RES
gbitmap_init_with_resource(&data->heart_icon, RESOURCE_ID_STRIDE_HEART);
data->steps_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_46_NUMBERS_AM_PM);
data->time_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_88_NUMBERS_AM_PM);
data->am_pm_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_88_THIN_NUMBERS_AM_PM);
#elif SNOWY_SCREEN_RES || SPALDING_SCREEN_RES
#if PBL_RECT
gbitmap_init_with_resource(&data->shoe_blue_small, RESOURCE_ID_STRIDE_SHOE_BLUE_SMALL);
gbitmap_init_with_resource(&data->shoe_green_small, RESOURCE_ID_STRIDE_SHOE_GREEN_SMALL);
#endif // PBL_RECT
data->steps_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_36_NUMBERS_AM_PM);
data->time_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_60_NUMBERS_AM_PM);
data->am_pm_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_60_THIN_NUMBERS_AM_PM);
#else
#error "Undefined screen size"
#endif // ROBERT_SCREEN_RES
#endif // PBL_BW
Layer *window_layer = window_get_root_layer(window);
// set window background
window_set_background_color(window, GColorBlack);
// set up the base layer
layer_init(&data->base_layer, &window_layer->bounds);
layer_set_update_proc(&data->base_layer, prv_base_layer_update_proc);
layer_add_child(window_layer, &data->base_layer);
// update steps and time
prv_update_steps_buffer(data);
// subscribe to health service
health_service_events_subscribe(prv_health_service_events_handler, data);
// subscribe to tick timer for minute ticks
tick_timer_service_subscribe(MINUTE_UNIT, prv_tick_handler);
}
T_STATIC void prv_window_unload_handler(Window *window) {
KickstartData *data = window_get_user_data(window);
// unsubscribe from service events
health_service_events_unsubscribe();
tick_timer_service_unsubscribe();
// deinit everything
#if PBL_BW
gbitmap_deinit(&data->shoe);
#else
gbitmap_deinit(&data->shoe_blue);
gbitmap_deinit(&data->shoe_green);
#endif
#if PBL_COLOR && SNOWY_SCREEN_RES
gbitmap_deinit(&data->shoe_blue_small);
gbitmap_deinit(&data->shoe_green_small);
#endif
gbitmap_deinit(&data->heart_icon);
layer_deinit(&data->base_layer);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// App Main
static void prv_main(void) {
KickstartData *data = app_zalloc_check(sizeof(KickstartData));
app_state_set_user_data(data);
prv_update_data(data);
window_init(&data->window, WINDOW_NAME("Kickstart"));
window_set_user_data(&data->window, data);
window_set_window_handlers(&data->window, &(WindowHandlers) {
.load = prv_window_load_handler,
.unload = prv_window_unload_handler,
});
app_window_stack_push(&data->window, true);
app_event_loop();
window_deinit(&data->window);
i18n_free_all(data);
app_free(data);
}
const PebbleProcessMd* kickstart_get_app_info() {
static const PebbleProcessMdSystem s_app_md = {
.common = {
// UUID: 3af858c3-16cb-4561-91e7-f1ad2df8725f
.uuid = {0x3a, 0xf8, 0x58, 0xc3, 0x16, 0xcb, 0x45, 0x61,
0x91, 0xe7, 0xf1, 0xad, 0x2d, 0xf8, 0x72, 0x5f},
.main_func = prv_main,
.process_type = ProcessTypeWatchface,
},
.icon_resource_id = RESOURCE_ID_MENU_ICON_KICKSTART_WATCH,
.name = "Kickstart"
};
return (const PebbleProcessMd*) &s_app_md;
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/ui/ui.h"
#include "process_management/pebble_process_md.h"
typedef struct KickstartData {
Window window;
Layer base_layer;
int32_t current_steps;
int32_t typical_steps;
int32_t daily_steps_avg;
int32_t current_bpm;
#if PBL_BW
GBitmap shoe;
#else
GBitmap shoe_blue;
GBitmap shoe_green;
#endif
#if PBL_COLOR && PBL_DISPLAY_WIDTH == 144 && PBL_DISPLAY_HEIGHT == 168
GBitmap shoe_blue_small;
GBitmap shoe_green_small;
#endif
GBitmap heart_icon;
GFont steps_font;
GFont time_font;
GFont am_pm_font;
bool screen_is_obstructed;
char steps_buffer[8];
} KickstartData;
const PebbleProcessMd* kickstart_get_app_info();

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "low_power_face.h"
#include "applib/app.h"
#include "applib/graphics/gdraw_command_image.h"
#include "applib/graphics/text.h"
#include "applib/tick_timer_service.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/ui.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/battery/battery_monitor.h"
#include "services/common/clock.h"
#include "util/time/time.h"
#include <string.h>
typedef struct {
Window low_power_window;
TextLayer low_power_time_layer;
KinoLayer low_power_kino_layer;
char time_text[6];
} LowPowerFaceData;
static LowPowerFaceData *s_low_power_data;
static void prv_minute_tick(struct tm *tick_time, TimeUnits units_changed) {
char *time_format;
if (clock_is_24h_style()) {
time_format = "%R";
} else {
time_format = "%I:%M";
}
strftime(s_low_power_data->time_text, sizeof(s_low_power_data->time_text),
time_format, tick_time);
// Remove leading zero from hour in case of 12h mode
if (!clock_is_24h_style() && (s_low_power_data->time_text[0] == '0')) {
text_layer_set_text(&s_low_power_data->low_power_time_layer,
s_low_power_data->time_text+1);
} else {
text_layer_set_text(&s_low_power_data->low_power_time_layer,
s_low_power_data->time_text);
}
}
static void deinit(void) {
tick_timer_service_unsubscribe();
kino_layer_deinit(&s_low_power_data->low_power_kino_layer);
app_free(s_low_power_data);
}
static void init(void) {
s_low_power_data = app_malloc_check(sizeof(*s_low_power_data));
window_init(&s_low_power_data->low_power_window, "Low Power");
window_set_background_color(&s_low_power_data->low_power_window, GColorLightGray);
app_window_stack_push(&s_low_power_data->low_power_window, true /* Animated */);
const GFont text_font = fonts_get_system_font(FONT_KEY_LECO_42_NUMBERS);
const GTextAlignment text_alignment = GTextAlignmentCenter;
const unsigned int font_height = fonts_get_font_height(text_font);
const GTextOverflowMode text_overflow_mode = GTextOverflowModeTrailingEllipsis;
const GSize text_size = app_graphics_text_layout_get_content_size("00:00", text_font,
s_low_power_data->low_power_window.layer.bounds, text_alignment,
text_overflow_mode);
const int text_pos_y_adjust = -9; // small vertical adjustment to match design specification
const int text_pos_y = (DISP_ROWS / 2) - (font_height / 2) + text_pos_y_adjust;
const GRect text_container_rect = GRect(0, text_pos_y, DISP_COLS, font_height);
GRect text_frame = (GRect) { .size = text_size };
grect_align(&text_frame, &text_container_rect, GAlignTop, false);
kino_layer_init(&s_low_power_data->low_power_kino_layer,
&s_low_power_data->low_power_window.layer.bounds);
kino_layer_set_reel_with_resource(&s_low_power_data->low_power_kino_layer,
RESOURCE_ID_BATTERY_NEEDS_CHARGING);
kino_layer_set_alignment(&s_low_power_data->low_power_kino_layer, GAlignBottom);
// TODO PBL-30180: Design needs to revise icon so it doesn't have a rounded cap at the bottom
s_low_power_data->low_power_kino_layer.layer.frame.origin.y += 2;
layer_add_child(&s_low_power_data->low_power_window.layer,
&s_low_power_data->low_power_kino_layer.layer);
text_layer_init_with_parameters(&s_low_power_data->low_power_time_layer,
&s_low_power_data->low_power_window.layer.frame, NULL, text_font,
GColorBlack, GColorClear, text_alignment, text_overflow_mode);
layer_set_frame(&s_low_power_data->low_power_time_layer.layer, &text_frame);
layer_add_child(&s_low_power_data->low_power_window.layer,
&s_low_power_data->low_power_time_layer.layer);
// Because of the delay before the tick timer service first calls prv_minute_tick,
// we call it ourselves to update the time right away
struct tm current_time;
clock_get_time_tm(&current_time);
prv_minute_tick(&current_time, HOUR_UNIT);
tick_timer_service_subscribe(MINUTE_UNIT, prv_minute_tick);
}
static void low_power_main(void) {
init();
app_event_loop();
deinit();
}
const PebbleProcessMd* low_power_face_get_app_info() {
static const PebbleProcessMdSystem s_app_md = {
.common = {
// UUID: e9475244-5bbe-4e0f-a637-a218af4c3110
.uuid = {0xe9, 0x47, 0x52, 0x44, 0x5b, 0xbe, 0x4e, 0x0f, 0xa6, 0x37, 0xa2, 0x18, 0xaf, 0x4c, 0x31, 0x10},
.main_func = low_power_main,
.process_type = ProcessTypeWatchface,
.visibility = ProcessVisibilityHidden,
},
.name = "Watch Only"
};
return (const PebbleProcessMd*) &s_app_md;
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "process_management/pebble_process_md.h"
const PebbleProcessMd* low_power_face_get_app_info();

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app.h"
#include "applib/rockyjs/rocky.h"
#include "applib/ui/app_window_stack.h"
#include "resource/resource_ids.auto.h"
void tictoc_main(void) {
// Push a window so we don't exit
Window *window = window_create();
app_window_stack_push(window, false/*animated*/);
#if CAPABILITY_HAS_JAVASCRIPT
rocky_event_loop_with_system_resource(RESOURCE_ID_JS_TICTOC);
#else
app_event_loop();
#endif
}

View File

@@ -0,0 +1,258 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "watch_model.h"
#include "applib/app.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/graphics_circle.h"
#include "applib/graphics/text.h"
#include "util/trig.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/ui.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/clock.h"
#include "util/time/time.h"
typedef struct {
Window window;
GFont text_font;
ClockModel clock_model;
GBitmap *bg_bitmap;
GPath *hour_path;
GPath *minute_path;
} MultiWatchData;
static const GPathInfo HOUR_PATH_INFO = {
.num_points = 9,
.points = (GPoint[]) {
{-5, 10}, {-2, 10}, {-2, 15}, {2, 15}, {2, 10}, {5, 10}, {5, -51}, {0, -56}, {-5, -51},
},
};
static const GPathInfo MINUTE_PATH_INFO = {
.num_points = 5,
.points = (GPoint[]) {
{-5, 10}, {5, 10}, {5, -61}, {0, -66}, {-5, -61},
},
};
void watch_model_handle_change(ClockModel *model) {
MultiWatchData *data = app_state_get_user_data();
data->clock_model = *model;
layer_mark_dirty(window_get_root_layer(&data->window));
}
static GPointPrecise prv_gpoint_from_polar(const GPointPrecise *center, uint32_t distance,
int32_t angle) {
return gpoint_from_polar_precise(center, distance << GPOINT_PRECISE_PRECISION, angle);
}
static void prv_graphics_draw_centered_text(GContext *ctx, const GSize *max_size,
const GPoint *center, const GFont font,
const GColor color, const char *text) {
GSize text_size = app_graphics_text_layout_get_content_size(
text, font, (GRect) { .size = *max_size }, GTextOverflowModeFill, GTextAlignmentCenter);
GPoint text_center = *center;
text_center.x -= text_size.w / 2 + 1;
text_center.y -= text_size.h * 2 / 3;
graphics_context_set_text_color(ctx, color);
graphics_draw_text(ctx, text, font, (GRect) { .origin = text_center, .size = text_size },
GTextOverflowModeFill, GTextAlignmentCenter, NULL);
}
static void prv_draw_watch_hand_rounded(GContext *ctx, ClockHand *hand, GPointPrecise center) {
GPointPrecise watch_hand_end = prv_gpoint_from_polar(&center, hand->length, hand->angle);
if (hand->style == CLOCK_HAND_STYLE_ROUNDED_WITH_HIGHLIGHT) {
graphics_context_set_stroke_color(ctx, GColorWhite);
graphics_line_draw_precise_stroked_aa(ctx, center, watch_hand_end, hand->thickness + 2);
}
graphics_context_set_stroke_color(ctx, hand->color);
graphics_line_draw_precise_stroked_aa(ctx, center, watch_hand_end, hand->thickness);
}
static void prv_draw_watch_hand_pointed(GContext *ctx, ClockHand *hand, GPoint center,
GPath *path) {
graphics_context_set_fill_color(ctx, hand->color);
gpath_rotate_to(path, hand->angle);
gpath_move_to(path, center);
gpath_draw_filled(ctx, path);
}
static void prv_draw_watch_hand(GContext *ctx, ClockHand *hand, GPointPrecise center, GPath *path) {
switch (hand->style) {
case CLOCK_HAND_STYLE_POINTED:
prv_draw_watch_hand_pointed(ctx, hand, GPointFromGPointPrecise(center), path);
case CLOCK_HAND_STYLE_ROUNDED:
case CLOCK_HAND_STYLE_ROUNDED_WITH_HIGHLIGHT:
default:
prv_draw_watch_hand_rounded(ctx, hand, center);
break;
}
}
static GPointPrecise prv_get_clock_center_point(ClockLocation location, const GRect *bounds) {
GPoint imprecise_center_point = {0};
switch (location) {
case CLOCK_LOCATION_TOP:
imprecise_center_point = (GPoint) {
.x = bounds->size.w / 2,
.y = bounds->size.h / 4,
};
case CLOCK_LOCATION_RIGHT:
imprecise_center_point = (GPoint) {
.x = bounds->size.w * 3 / 4 - 5,
.y = bounds->size.h / 2,
};
case CLOCK_LOCATION_BOTTOM:
imprecise_center_point = (GPoint) {
.x = bounds->size.w / 2,
.y = bounds->size.h * 3 / 4 + 6,
};
case CLOCK_LOCATION_LEFT:
imprecise_center_point = (GPoint) {
.x = bounds->size.w / 4 + 4,
.y = bounds->size.h / 2,
};
default:
// aiming for width / 2 - 0.5 to get the true center
return (GPointPrecise) {
.x = { .integer = bounds->size.w / 2 - 1, .fraction = 3 },
.y = { .integer = bounds->size.h / 2 - 1, .fraction = 3 }
};
}
return GPointPreciseFromGPoint(imprecise_center_point);
}
static void prv_draw_clock_face(GContext *ctx, ClockFace *face) {
MultiWatchData *data = app_state_get_user_data();
const GRect *bounds = &window_get_root_layer(&data->window)->bounds;
const GPointPrecise center = prv_get_clock_center_point(face->location, bounds);
// Draw hands.
// TODO: Need to do something about the static GPaths used for watchands. This is very inflexible.
prv_draw_watch_hand(ctx, &face->hour_hand, center, data->hour_path);
prv_draw_watch_hand(ctx, &face->minute_hand, center, data->minute_path);
// Draw bob.
GRect bob_rect = (GRect) {
.size = GSize(face->bob_radius * 2, face->bob_radius * 2)
};
GRect bob_center_rect = (GRect) {
.size = GSize(face->bob_center_radius * 2, face->bob_center_radius * 2)
};
grect_align(&bob_rect, bounds, GAlignCenter, false /* clips */);
grect_align(&bob_center_rect, bounds, GAlignCenter, false /* clips */);
graphics_context_set_fill_color(ctx, face->bob_color);
graphics_fill_oval(ctx, bob_rect, GOvalScaleModeFitCircle);
graphics_context_set_fill_color(ctx, face->bob_center_color);
graphics_fill_oval(ctx, bob_center_rect, GOvalScaleModeFitCircle);
}
static void prv_draw_non_local_clock(GContext *ctx, NonLocalClockFace *clock) {
// TODO: The non-local clock text is currently baked into the background image.
prv_draw_clock_face(ctx, &clock->face);
}
static void prv_update_proc(Layer *layer, GContext *ctx) {
MultiWatchData *data = app_state_get_user_data();
const GRect *bounds = &layer->bounds;
// Background.
graphics_draw_bitmap_in_rect(ctx, data->bg_bitmap, bounds);
ClockModel *clock_model = &data->clock_model;
// Watch text. TODO: Locate the text properly, rather than hard-coding.
if (clock_model->text.location == CLOCK_TEXT_LOCATION_BOTTOM) {
const GPoint point = (GPoint) { 90, 140 };
prv_graphics_draw_centered_text(ctx, &bounds->size, &point, data->text_font,
clock_model->text.color, clock_model->text.buffer);
} else if (clock_model->text.location == CLOCK_TEXT_LOCATION_LEFT) {
const GRect box = (GRect) { .origin = GPoint(25, 78), .size = bounds->size };
graphics_draw_text(ctx, clock_model->text.buffer, data->text_font, box,
GTextOverflowModeFill, GTextAlignmentLeft, NULL);
}
// Draw the clocks.
for (uint32_t i = 0; i < clock_model->num_non_local_clocks; ++i) {
prv_draw_non_local_clock(ctx, &clock_model->non_local_clock[i]);
}
prv_draw_clock_face(ctx, &clock_model->local_clock);
}
static void prv_window_load(Window *window) {
MultiWatchData *data = app_state_get_user_data();
layer_set_update_proc(window_get_root_layer(window), prv_update_proc);
watch_model_init();
data->hour_path = gpath_create(&HOUR_PATH_INFO);
data->minute_path = gpath_create(&MINUTE_PATH_INFO);
data->bg_bitmap = gbitmap_create_with_resource(data->clock_model.bg_bitmap_id);
}
static void prv_window_unload(Window *window) {
MultiWatchData *data = app_state_get_user_data();
gpath_destroy(data->hour_path);
gpath_destroy(data->minute_path);
gbitmap_destroy(data->bg_bitmap);
}
static void prv_app_did_focus(bool did_focus) {
if (!did_focus) {
return;
}
app_focus_service_unsubscribe();
watch_model_start_intro();
}
static void prv_init(void) {
MultiWatchData *data = app_zalloc_check(sizeof(*data));
app_state_set_user_data(data);
data->text_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
window_init(&data->window, "TicToc");
window_set_window_handlers(&data->window, &(WindowHandlers) {
.load = prv_window_load,
.unload = prv_window_unload,
});
const bool animated = true;
app_window_stack_push(&data->window, animated);
app_focus_service_subscribe_handlers((AppFocusHandlers) {
.did_focus = prv_app_did_focus,
});
}
static void prv_deinit(void) {
MultiWatchData *data = app_state_get_user_data();
window_destroy(&data->window);
watch_model_cleanup();
}
void tictoc_main(void) {
prv_init();
app_event_loop();
prv_deinit();
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "watch_model.h"
#include "util/trig.h"
#include "applib/app_watch_info.h"
#include "applib/pbl_std/pbl_std.h"
#include "applib/tick_timer_service.h"
#include "resource/resource_ids.auto.h"
#include "syscall/syscall.h"
#include "util/time/time.h"
#include <ctype.h>
static void prv_calculate_hand_angles(struct tm *tick_time, int32_t *hour_angle,
int32_t *minute_angle) {
*hour_angle = (tick_time->tm_hour % 12) * TRIG_MAX_ANGLE / 12
+ tick_time->tm_min * TRIG_MAX_ANGLE / 60 / 12;
*minute_angle = tick_time->tm_min * TRIG_MAX_ANGLE / 60;
}
static ClockFace prv_local_clock_face_default(struct tm *tick_time) {
int32_t hour_angle, minute_angle;
prv_calculate_hand_angles(tick_time, &hour_angle, &minute_angle);
// TODO: Don't return by value. This thing is massive.
return (ClockFace) {
.hour_hand = {
.angle = hour_angle,
.backwards_extension = LOCAL_HOUR_HAND_BACK_EXT_DEFAULT,
.color = LOCAL_HOUR_HAND_COLOR_DEFAULT,
.length = LOCAL_HOUR_HAND_LENGTH_DEFAULT,
.style = CLOCK_HAND_STYLE_ROUNDED,
.thickness = LOCAL_HOUR_HAND_THICKNESS_DEFAULT,
},
.minute_hand = {
.angle = minute_angle,
.backwards_extension = LOCAL_MINUTE_HAND_BACK_EXT_DEFAULT,
.color = LOCAL_MINUTE_HAND_COLOR_DEFAULT,
.length = LOCAL_MINUTE_HAND_LENGTH_DEFAULT,
.style = CLOCK_HAND_STYLE_ROUNDED,
.thickness = LOCAL_MINUTE_HAND_THICKNESS_DEFAULT,
},
.bob_radius = LOCAL_BOB_RADIUS_DEFAULT,
.bob_color = LOCAL_BOB_COLOR_DEFAULT,
.location = CLOCK_LOCATION_CENTER,
};
}
static NonLocalClockFace prv_configure_non_local_clock_face(int32_t utc_offset, const char *text,
GColor text_color, GColor hand_color,
ClockTextLocation location) {
time_t t = rtc_get_time();
struct tm* tick_time = pbl_override_gmtime(&t);
tick_time->tm_hour += utc_offset; // TODO check if this works properly
int32_t hour_angle, minute_angle;
prv_calculate_hand_angles(tick_time, &hour_angle, &minute_angle);
// TODO: Don't return by value. This thing is massive.
NonLocalClockFace non_local_clock = (NonLocalClockFace) {
.face = {
.hour_hand = {
.length = NON_LOCAL_HOUR_HAND_LENGTH_DEFAULT,
.thickness = NON_LOCAL_HOUR_HAND_WIDTH_DEFAULT,
.backwards_extension = 0,
.angle = hour_angle,
.color = hand_color,
.style = CLOCK_HAND_STYLE_ROUNDED,
},
.minute_hand = {
.length = NON_LOCAL_MINUTE_HAND_LENGTH_DEFAULT,
.thickness = NON_LOCAL_MINUTE_HAND_WIDTH_DEFAULT,
.backwards_extension = 0,
.angle = minute_angle,
.color = hand_color,
.style = CLOCK_HAND_STYLE_ROUNDED,
},
.location = location,
},
.text_color = text_color,
};
strncpy(non_local_clock.buffer, text, sizeof(non_local_clock.buffer));
return non_local_clock;
}
// Configure the text displayed on the clock.
static ClockText prv_configure_clock_text(ClockTextType type, ClockTextLocation location,
GColor color, struct tm *tick_time) {
ClockText text = (ClockText) {
.location = location,
.color = color,
};
if (type == CLOCK_TEXT_TYPE_DATE) {
strftime(text.buffer, sizeof(text.buffer), "%a %d", tick_time);
} else if (type == CLOCK_TEXT_TYPE_TIME) {
strftime(text.buffer, sizeof(text.buffer), "$l:%M%P", tick_time);
}
for (uint32_t i = 0; i < sizeof(text.buffer); i++) {
text.buffer[i] = toupper((unsigned char)text.buffer[i]);
}
// TODO: Don't return a struct
return text;
}
static ClockModel prv_clock_model_default(struct tm *tick_time) {
// Create a generic model and configure a default clock.
ClockModel model;
model.local_clock = prv_local_clock_face_default(tick_time);
// Add watch-specific details.
const WatchInfoColor watch_color = sys_watch_info_get_color();
switch (watch_color) {
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_14:
model.local_clock.minute_hand.color = GColorBlue;
model.text = prv_configure_clock_text(CLOCK_TEXT_TYPE_DATE, CLOCK_TEXT_LOCATION_LEFT,
GColorWhite, tick_time);
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_14MM_BLACK_RED;
break;
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_20:
model.num_non_local_clocks = 2;
model.non_local_clock[0] = prv_configure_non_local_clock_face(-7, "LA", GColorDarkGray,
GColorWhite,
CLOCK_LOCATION_LEFT);
model.non_local_clock[1] = prv_configure_non_local_clock_face(2, "PAR", GColorDarkGray,
GColorWhite,
CLOCK_LOCATION_RIGHT);
model.text = prv_configure_clock_text(CLOCK_TEXT_TYPE_DATE, CLOCK_TEXT_LOCATION_BOTTOM,
GColorWhite, tick_time);
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_20MM_BLACK;
break;
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_14:
model.local_clock.hour_hand.style = CLOCK_HAND_STYLE_POINTED;
model.local_clock.hour_hand.color = GColorBlack;
model.local_clock.minute_hand.style = CLOCK_HAND_STYLE_POINTED;
model.local_clock.minute_hand.color = GColorCadetBlue;
model.text = prv_configure_clock_text(CLOCK_TEXT_TYPE_DATE, CLOCK_TEXT_LOCATION_BOTTOM,
GColorDarkGray, tick_time);
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_14MM_SILVER;
break;
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_20:
model.local_clock.hour_hand.style = CLOCK_HAND_STYLE_POINTED;
model.local_clock.minute_hand.style = CLOCK_HAND_STYLE_POINTED;
model.local_clock.minute_hand.color = GColorRed;
model.local_clock.bob_color = GColorBlack;
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_20MM_SILVER_BROWN;
break;
case WATCH_INFO_COLOR_TIME_ROUND_ROSE_GOLD_14:
default:
model.local_clock.bob_center_color = GColorOrange;
model.local_clock.minute_hand.color = GColorWhite;
model.local_clock.minute_hand.thickness = 2;
model.local_clock.minute_hand.length = 54;
model.local_clock.hour_hand.color = GColorBlack;
model.local_clock.hour_hand.thickness = 8;
model.local_clock.hour_hand.length = 39;
model.local_clock.bob_radius = 7;
model.local_clock.bob_center_radius = 3;
model.local_clock.bob_color = GColorWhite;
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_14MM_ROSE_GOLD;
break;
}
// disable timezones until they can be configured by the user
model.num_non_local_clocks = 0;
// TODO: Don't return a struct here
return model;
}
static void prv_handle_time_update(struct tm *tick_time, TimeUnits units_changed) {
ClockModel model = prv_clock_model_default(tick_time);
watch_model_handle_change(&model);
}
void watch_model_cleanup() {
tick_timer_service_unsubscribe();
}
static void prv_intro_animation_finished(Animation *animation) {
const time_t t = rtc_get_time();
prv_handle_time_update(pbl_override_localtime(&t), (TimeUnits)0xff);
tick_timer_service_subscribe(MINUTE_UNIT, prv_handle_time_update);
}
void watch_model_start_intro() {
prv_intro_animation_finished(NULL);
}
void watch_model_init(void) {
const time_t t = rtc_get_time();
struct tm *tick_time = pbl_override_localtime(&t);
ClockModel model = prv_clock_model_default(tick_time);
watch_model_handle_change(&model);
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include <inttypes.h>
#define LOCAL_HOUR_HAND_LENGTH_DEFAULT 51
#define LOCAL_HOUR_HAND_THICKNESS_DEFAULT 6
#define LOCAL_HOUR_HAND_COLOR_DEFAULT GColorWhite
#define LOCAL_HOUR_HAND_BACK_EXT_DEFAULT 0
#define LOCAL_MINUTE_HAND_LENGTH_DEFAULT 58
#define LOCAL_MINUTE_HAND_THICKNESS_DEFAULT 6
#define LOCAL_MINUTE_HAND_COLOR_DEFAULT GColorWhite
#define LOCAL_MINUTE_HAND_BACK_EXT_DEFAULT 0
#define LOCAL_BOB_RADIUS_DEFAULT 6
#define LOCAL_BOB_COLOR_DEFAULT GColorRed
#define NON_LOCAL_HOUR_HAND_LENGTH_DEFAULT 11
#define NON_LOCAL_HOUR_HAND_WIDTH_DEFAULT 3
#define NON_LOCAL_MINUTE_HAND_LENGTH_DEFAULT 21
#define NON_LOCAL_MINUTE_HAND_WIDTH_DEFAULT 3
#define NUM_NON_LOCAL_CLOCKS 3
#define GLANCE_TIME_OUT_MS 8000
typedef enum {
CLOCK_TEXT_TYPE_NONE = 0,
CLOCK_TEXT_TYPE_TIME,
CLOCK_TEXT_TYPE_DATE,
} ClockTextType;
typedef enum {
CLOCK_TEXT_LOCATION_NONE = 0,
CLOCK_TEXT_LOCATION_BOTTOM,
CLOCK_TEXT_LOCATION_LEFT,
} ClockTextLocation;
typedef enum {
CLOCK_HAND_STYLE_ROUNDED = 0,
CLOCK_HAND_STYLE_ROUNDED_WITH_HIGHLIGHT,
CLOCK_HAND_STYLE_POINTED,
} ClockHandStyle;
typedef enum {
CLOCK_LOCATION_CENTER,
CLOCK_LOCATION_LEFT,
CLOCK_LOCATION_BOTTOM,
CLOCK_LOCATION_RIGHT,
CLOCK_LOCATION_TOP,
} ClockLocation;
typedef struct {
uint16_t length;
uint16_t thickness;
uint16_t backwards_extension;
int32_t angle;
GColor color;
ClockHandStyle style;
} ClockHand;
typedef struct {
ClockHand hour_hand;
ClockHand minute_hand;
uint16_t bob_radius;
uint16_t bob_center_radius;
GColor bob_color;
GColor bob_center_color;
ClockLocation location;
} ClockFace;
typedef struct {
ClockFace face;
char buffer[4];
int32_t utc_offest;
GColor text_color;
} NonLocalClockFace;
typedef struct {
ClockTextType type;
ClockTextLocation location;
char buffer[10]; // FIXME magic number
GColor color;
} ClockText;
typedef struct {
ClockFace local_clock;
uint32_t num_non_local_clocks;
NonLocalClockFace non_local_clock[NUM_NON_LOCAL_CLOCKS];
ClockText text;
uint32_t bg_bitmap_id;
} ClockModel;
void watch_model_init(void);
void watch_model_handle_change(ClockModel *model);
void watch_model_start_intro(void);
void watch_model_cleanup(void);

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "tictoc.h"
#include "resource/resource_ids.auto.h"
const PebbleProcessMd* tictoc_get_app_info(void) {
static const PebbleProcessMdSystem s_app_md = {
.common = {
// UUID: 8f3c8686-31a1-4f5f-91f5-01600c9bdc59
.uuid = { 0x8f, 0x3c, 0x86, 0x86, 0x31, 0xa1, 0x4f, 0x5f,
0x91, 0xf5, 0x01, 0x60, 0x0c, 0x9b, 0xdc, 0x59 },
.main_func = tictoc_main,
.process_type = ProcessTypeWatchface,
#if CAPABILITY_HAS_JAVASCRIPT && !defined(PLATFORM_SPALDING)
.is_rocky_app = true,
#endif
},
.icon_resource_id = RESOURCE_ID_MENU_ICON_TICTOC_WATCH,
.name = "TicToc",
};
return (const PebbleProcessMd*) &s_app_md;
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "process_management/pebble_process_md.h"
void tictoc_main(void);
const PebbleProcessMd* tictoc_get_app_info(void);