#include #include "analog_wd.h" #include "constants.h" #include "options.h" #include "weather.h" #include "date.h" #include "messaging.h" #include "digital_time.h" #include "battery.h" #define ANTIALIASING true #define HAND_MARGIN 18 #define FINAL_RADIUS 80 #define REGULAR_HANDS_WIDTH 6 #define THIN_HANDS_WIDTH 5 #define ANIMATION_DURATION 500 #define ANIMATION_DELAY 600 #define HOUR_BAR_MARGIN 5 #define SCREEN_SHOT_HR 18 #define SCREEN_SHOT_MIN 22 #define HAND_LENGTH_SEC 75 #define SECONDS_HAND_DISPLAY_TIME_SECS 18 int g_display_seconds_hand_count = 0; //stores brief counter to display seconds hand Options s_options; //stores weather retry counts (incremented in second tick handler). //Stop retry count at DEFAULT_MAX_WEATHER_RETRY_COUNT int g_weather_retry_count = 0; typedef struct { int hours; int minutes; int seconds; } Time; static GPoint s_center; static Time s_last_time, s_anim_time; static int s_radius = 0, s_color_channels[3]; static bool s_animating = false; void set_text_layers_color(GColor textColor) { text_layer_set_text_color(weather_layer1, textColor); text_layer_set_text_color(weather_layer2, textColor); text_layer_set_text_color(weather_layer_center, textColor); text_layer_set_text_color(date_layer1, textColor); text_layer_set_text_color(date_layer2, textColor); text_layer_set_text_color(battery_layer, textColor); } static void init_window_background_color(){ window_set_background_color(s_main_window, GColorBlack); window_set_background_color(s_main_window, GColorFromHEX(s_options.background_color)); } void init_static_row(TextLayer *label, GFont font) { text_layer_set_background_color(label, GColorClear); text_layer_set_text_color(label, GColorWhite); text_layer_set_text_color(label, GColorFromHEX(s_options.text_color)); if (font) { text_layer_set_font(label, font); } } /*************************** AnimationImplementation **************************/ static void animation_started(Animation *anim, void *context) { s_animating = true; } static void animation_stopped(Animation *anim, bool stopped, void *context) { s_animating = false; } static void animate(int duration, int delay, AnimationImplementation *implementation, bool handlers) { Animation *anim = animation_create(); animation_set_duration(anim, duration); animation_set_delay(anim, delay); animation_set_curve(anim, AnimationCurveEaseInOut); animation_set_implementation(anim, implementation); if(handlers) { animation_set_handlers(anim, (AnimationHandlers) { .started = animation_started, .stopped = animation_stopped }, NULL); } animation_schedule(anim); } /************************************ UI **************************************/ static void bt_handler(bool isConnected) { int current_bluetooth_state = 0; //default bluetooth disconnected if (isConnected) { current_bluetooth_state = 1; //bluetooth connected } //if bluetooth was previously connected and is now disconnected if (s_options.bluetooth_state == 1 && current_bluetooth_state == 0) { if (s_options.vibrate_bt_status) { vibes_short_pulse(); //vibrate on bt disconnect, if option enabled if(s_canvas_layer) { layer_mark_dirty(s_canvas_layer); } } } //if bluetooth was previously disconnected and is now connected if (s_options.bluetooth_state == 0 && current_bluetooth_state == 1) { //clear weather conditions and reset weather retry count to force weather update memset(s_options.conditions, 0,sizeof(s_options.conditions)); s_options.condition_code = DEFAULT_CONDITION_CODE; g_weather_retry_count = 0; } //store bluetooth state s_options.bluetooth_state = current_bluetooth_state; } static void tap_handler(AccelAxisType axis, int32_t direction) { if (s_options.display_seconds_hand > 0) { time_t t = time(NULL); struct tm *time_now = localtime(&t); s_last_time.seconds = time_now->tm_sec; g_display_seconds_hand_count = SECONDS_HAND_DISPLAY_TIME_SECS; } if (s_options.shake_for_lohi == 1) { //Display Lo Hi temp info; display_lohi_weather_info(); } } //every five seconds static void handle_5second_tick(struct tm *tick_time, TimeUnits units_changed) { //if conditions blank (no current weather info) if(strlen(s_options.conditions) == 0 && g_weather_retry_count < DEFAULT_MAX_WEATHER_RETRY_COUNT) { //add one to weather retry count - stop trying this app session at DEFAULT_MAX_WEATHER_RETRY_COUNT ++g_weather_retry_count; //reset time since last forecast timer s_options.min_since_last_forecast = 0; //get updated weather send(KEY_GET_WEATHER, 1, KEY_WEATHER_USE_GPS, s_options.weather_use_GPS, KEY_WEATHER_LOCATION, s_options.weather_location); //update weather layer update_weather_layer(); } //APP_LOG(APP_LOG_LEVEL_DEBUG, "heap_bytes_free:heap_bytes_used %zu: %zu", heap_bytes_free(), heap_bytes_used()); } //every second static void handle_second_tick(struct tm *tick_time, TimeUnits units_changed) { if((s_options.display_seconds_hand == 1 && g_display_seconds_hand_count > 0) || s_options.display_seconds_hand == 2) { s_last_time.seconds = tick_time->tm_sec; if(s_canvas_layer) { layer_mark_dirty(s_canvas_layer); } } } //every minute static void handle_minute_tick(struct tm *tick_time, TimeUnits units_changed) { // Store time s_last_time.hours = tick_time->tm_hour; s_last_time.hours -= (s_last_time.hours > 12) ? 12 : 0; s_last_time.minutes = tick_time->tm_min; for(int i = 0; i < 3; i++) { s_color_channels[i] = 0; } // Redraw if(s_canvas_layer) { layer_mark_dirty(s_canvas_layer); } if (units_changed & HOUR_UNIT) { //vibes_double_pulse(); //hourly vibration } update_date_layer(); //check/update date layer update_digital_time_layer(); //check/update digital_time_layer update_battery_layer(); //check/update update_battery_layer //get minutes since last weather update time_t t = time(NULL); int min_since_last_update = (difftime(t, s_options.last_update) / 60) + 1; APP_LOG(APP_LOG_LEVEL_DEBUG, " (Last update %s) min_since_last_update:weather_frequency - %d:%d", s_options.last_weather_update24hr, min_since_last_update, s_options.weather_frequency); //APP_LOG(APP_LOG_LEVEL_DEBUG, "handle_minute_tick strlen(s_options.conditions): %zu", strlen(s_options.conditions)); //if min_since_last_forecast greater than or equal to weatherUpdateFrequency if(min_since_last_update >= s_options.weather_frequency || strlen(s_options.conditions) == 0) { s_options.min_since_last_forecast = 0; //get updated weather send(KEY_GET_WEATHER, 1, KEY_WEATHER_USE_GPS, s_options.weather_use_GPS, KEY_WEATHER_LOCATION, s_options.weather_location); //update weather layer update_weather_layer(); } } void handle_tick(struct tm *tick_time, TimeUnits units_changed) { if (units_changed & SECOND_UNIT) { handle_second_tick(tick_time, units_changed); if (tick_time->tm_sec % 5 == 0) { handle_5second_tick(tick_time, units_changed); } } if (units_changed & MINUTE_UNIT) { handle_minute_tick(tick_time, units_changed); } } static int hours_to_minutes(int hours_out_of_12) { return (int)(float)(((float)hours_out_of_12 / 12.0F) * 60.0F); } static void update_proc(Layer *layer, GContext *ctx) { GRect layer_frame = layer_get_frame(window_get_root_layer(s_main_window)); const int16_t width = layer_frame.size.w; const int16_t height = layer_frame.size.h; graphics_context_set_stroke_color(ctx, GColorWhite); graphics_context_set_antialiased(ctx, ANTIALIASING); /************************************ Minute lines **************************************/ for (int i = 0; i < 60; i++) { // hour bar if (i % 1 == 0 && i / 1 != -1) { GPoint minute_line = (GPoint) { .x = (int16_t)(sin_lookup(TRIG_MAX_ANGLE * i / 60) * (int32_t)(s_radius - HOUR_BAR_MARGIN) / TRIG_MAX_RATIO) + s_center.x, .y = (int16_t)(-cos_lookup(TRIG_MAX_ANGLE * i / 60) * (int32_t)(s_radius - HOUR_BAR_MARGIN) / TRIG_MAX_RATIO) + s_center.y, }; GPoint line_start = minute_line; time_t t = time(NULL); struct tm *time_now = localtime(&t); #if DEBUG time_now->tm_hour = SCREEN_SHOT_HR; //for screen shots time_now->tm_min = SCREEN_SHOT_MIN; #endif //emphasize/display hour indicators if (i % 5 == 0) { if (!s_options.display_hour_digits) { // emphasis graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.hour_markers_color)); line_start.x += (s_center.x - minute_line.x) * 0.12; line_start.y += (s_center.y - minute_line.y) * 0.12; graphics_context_set_stroke_width(ctx, 2); graphics_draw_line(ctx, line_start, minute_line); } else { graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.hour_markers_color)); line_start.x += (s_center.x - minute_line.x) * 0.02; line_start.y += (s_center.y - minute_line.y) * 0.02; graphics_context_set_stroke_width(ctx, 2); graphics_draw_line(ctx, line_start, minute_line); // show hour digits line_start.x += (s_center.x - line_start.x) * 0.11; line_start.y += (s_center.y - line_start.y) * 0.11; line_start.x -= 14; line_start.y -= 12; GSize sz = (GSize) { .w = 30, .h = 20, }; GRect rect = (GRect) { .origin = line_start, .size = sz, }; char str[3]; snprintf(str, 3, "%d", i / 5 == 0 ? 12 : i / 5); if (clock_is_24h_style() && time_now->tm_hour > 12) { snprintf(str, 3, "%d", i / 5 == 0 ? 24 : i / 5 + 12); } graphics_context_set_text_color(ctx, GColorFromHEX(s_options.hour_markers_color)); graphics_draw_text(ctx, str, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), rect, GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL); } } else { if (!s_options.display_hour_digits) { graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.minor_markers_color)); line_start.x += (s_center.x - minute_line.x) * 0.07; line_start.y += (s_center.y - minute_line.y) * 0.07; graphics_context_set_stroke_width(ctx, 1); graphics_draw_line(ctx, line_start, minute_line); } else { graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.minor_markers_color)); graphics_context_set_fill_color(ctx, GColorFromHEX(s_options.minor_markers_color)); graphics_fill_circle(ctx, minute_line, 1); } } } } /************************************ Watch hand width **************************************/ //set watch hand width if (s_options.use_thin_hands) { graphics_context_set_stroke_width(ctx, THIN_HANDS_WIDTH); } else { graphics_context_set_stroke_width(ctx, REGULAR_HANDS_WIDTH); } /************************************ Twelve o'clock dot **************************************/ //if background color the same color as the hour marker color, dispaly a dot at twelve o'clock position int chalk_offset = 10; if (s_options.background_color == s_options.hour_markers_color) { graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.hour_hand_color)); graphics_draw_circle(ctx, GPoint(width / 2, chalk_offset), 1); //twelve } /************************************ Hour and minute hands **************************************/ // Don't use current time while animating Time mode_time = (s_animating) ? s_anim_time : s_last_time; // Adjust for minutes through the hour float minute_angle = TRIG_MAX_ANGLE * mode_time.minutes / 60; float hour_angle; if(s_animating) { // Hours out of 60 for smoothness hour_angle = TRIG_MAX_ANGLE * mode_time.hours / 60; } else { hour_angle = TRIG_MAX_ANGLE * mode_time.hours / 12; } hour_angle += (minute_angle / TRIG_MAX_ANGLE) * (TRIG_MAX_ANGLE / 12); // Plot hands GPoint minute_hand = (GPoint) { .x = (int16_t)(sin_lookup(TRIG_MAX_ANGLE * mode_time.minutes / 60) * (int32_t)(s_radius - HAND_MARGIN) / TRIG_MAX_RATIO) + s_center.x, .y = (int16_t)(-cos_lookup(TRIG_MAX_ANGLE * mode_time.minutes / 60) * (int32_t)(s_radius - HAND_MARGIN) / TRIG_MAX_RATIO) + s_center.y, }; GPoint hour_hand = (GPoint) { .x = (int16_t)(sin_lookup(hour_angle) * (int32_t)(s_radius - (1.75 * HAND_MARGIN)) / TRIG_MAX_RATIO) + s_center.x, .y = (int16_t)(-cos_lookup(hour_angle) * (int32_t)(s_radius - (1.75 * HAND_MARGIN)) / TRIG_MAX_RATIO) + s_center.y, }; // Draw hands with positive length only if(s_radius > HAND_MARGIN) { graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.minute_hand_color)); graphics_draw_line(ctx, s_center, minute_hand); } if(s_radius > 2 * HAND_MARGIN) { graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.hour_hand_color)); if (s_options.vibrate_bt_status) { if (s_options.bluetooth_state == 0) { graphics_context_set_stroke_color(ctx, GColorBlue); } } graphics_draw_line(ctx, s_center, hour_hand); //center circle graphics_draw_circle(ctx, GPoint(width / 2, height / 2), 1); } /************************************ Seconds hand line **************************************/ // Draw seconds hand --g_display_seconds_hand_count; if (g_display_seconds_hand_count < 0) g_display_seconds_hand_count = 0; if((s_options.display_seconds_hand == 1 && g_display_seconds_hand_count > 0) || (s_options.display_seconds_hand == 2)) { // Plot seconds hand ends GPoint second_hand_long = (GPoint) { .x = (int16_t)(sin_lookup(TRIG_MAX_ANGLE * s_last_time.seconds / 60) * (int32_t)HAND_LENGTH_SEC / TRIG_MAX_RATIO) + s_center.x, .y = (int16_t)(-cos_lookup(TRIG_MAX_ANGLE * s_last_time.seconds / 60) * (int32_t)HAND_LENGTH_SEC / TRIG_MAX_RATIO) + s_center.y, }; graphics_context_set_stroke_width(ctx, 2); graphics_context_set_stroke_color(ctx, GColorFromHEX(s_options.seconds_hand_color)); graphics_draw_line(ctx, GPoint(s_center.x, s_center.y), GPoint(second_hand_long.x, second_hand_long.y)); } } static void window_load(Window *window) { Layer *window_layer = window_get_root_layer(window); GRect window_bounds = layer_get_bounds(window_layer); s_center = grect_center_point(&window_bounds); s_canvas_layer = layer_create(window_bounds); layer_set_update_proc(s_canvas_layer, update_proc); layer_add_child(window_layer, s_canvas_layer); } static void window_unload(Window *window) { layer_destroy(s_canvas_layer); } /*********************************** App **************************************/ static int anim_percentage(AnimationProgress dist_normalized, int max) { return (int)(float)(((float)dist_normalized / (float)ANIMATION_NORMALIZED_MAX) * (float)max); } static void radius_update(Animation *anim, AnimationProgress dist_normalized) { int chalkRadiusAdjustment = 0; #if defined(PBL_ROUND) chalkRadiusAdjustment = 12; #endif s_radius = anim_percentage(dist_normalized, FINAL_RADIUS + chalkRadiusAdjustment); layer_mark_dirty(s_canvas_layer); } static void hands_update(Animation *anim, AnimationProgress dist_normalized) { s_anim_time.hours = anim_percentage(dist_normalized, hours_to_minutes(s_last_time.hours)); s_anim_time.minutes = anim_percentage(dist_normalized, s_last_time.minutes); layer_mark_dirty(s_canvas_layer); } static void handle_deinit() { //save watchface options persist_write_data(KEY_OPTIONS, &s_options, sizeof(s_options)); text_layer_destroy(weather_layer1); text_layer_destroy(weather_layer_center); text_layer_destroy(weather_layer2); text_layer_destroy(digital_time_layer); text_layer_destroy(battery_layer); text_layer_destroy(date_layer1); text_layer_destroy(date_layer2); connection_service_unsubscribe(); accel_tap_service_unsubscribe(); tick_timer_service_unsubscribe(); window_destroy(s_main_window); } static void handle_init() { //light_enable(true); //background light for screen shots srand(time(NULL)); s_main_window = window_create(); window_set_window_handlers(s_main_window, (WindowHandlers) { .load = window_load, .unload = window_unload, }); //set-retrieve watchface options init_options(); init_window_background_color(); // Prepare animations AnimationImplementation radius_impl = { .update = radius_update }; animate(ANIMATION_DURATION, ANIMATION_DELAY, &radius_impl, false); AnimationImplementation hands_impl = { .update = hands_update }; animate(2 * ANIMATION_DURATION, ANIMATION_DELAY, &hands_impl, true); //add weather and date layers GRect layer_frame = layer_get_frame(window_get_root_layer(s_main_window)); const int16_t width = layer_frame.size.w; const int16_t height = layer_frame.size.h; //add_weatherdate_layers(window_get_root_layer(s_main_window), width, height); add_date_layers(window_get_root_layer(s_main_window), width, height); add_weather_layers(window_get_root_layer(s_main_window), width, height); add_digital_time_layer(window_get_root_layer(s_main_window), width, height); add_battery_layer(window_get_root_layer(s_main_window), width, height); app_message_register_inbox_received(inbox_received_callback); app_message_register_inbox_dropped(message_dropped); app_message_register_outbox_sent(message_out_success); app_message_register_outbox_failed(message_out_failed); app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum()); update_date_layer(); //update date_layer update_digital_time_layer(); //update digital_time_layer update_battery_layer(); //update update_battery_layer update_weather_layer(); //update weather layer tick_timer_service_subscribe(MINUTE_UNIT | SECOND_UNIT, handle_tick); accel_tap_service_subscribe(tap_handler); time_t t = time(NULL); struct tm *time_now = localtime(&t); #if DEBUG time_now->tm_hour = SCREEN_SHOT_HR; //for screen shots time_now->tm_min = SCREEN_SHOT_MIN; #endif if (s_options.display_seconds_hand > 0) s_last_time.seconds = time_now->tm_sec; handle_tick(time_now, MINUTE_UNIT); //get minutes since last weather update //int min_since_last_update = difftime(t, s_options.last_update) / 60; //APP_LOG(APP_LOG_LEVEL_DEBUG, "******************* minutes since last weather update %d: weather_frequency %d", min_since_last_update, s_options.weather_frequency); // Subscribe to Bluetooth updates connection_service_subscribe((ConnectionHandlers) { .pebble_app_connection_handler = bt_handler }); // Check current bluetooth connection state bt_handler(connection_service_peek_pebble_app_connection()); const bool animated = true; window_stack_push(s_main_window, animated); } int main() { handle_init(); app_event_loop(); handle_deinit(); }