diff options
| -rw-r--r-- | src/c/fastforge.c | 62 | ||||
| -rw-r--r-- | src/c/fastforge_core.c | 12 | ||||
| -rw-r--r-- | src/c/fastforge_internal.h | 1 | ||||
| -rw-r--r-- | src/c/fastforge_logic.c | 96 |
4 files changed, 102 insertions, 69 deletions
diff --git a/src/c/fastforge.c b/src/c/fastforge.c index 578948e..8d410a7 100644 --- a/src/c/fastforge.c +++ b/src/c/fastforge.c @@ -27,7 +27,8 @@ enum { PRESET_MENU_INDEX_28H = 5, PRESET_MENU_INDEX_30H = 6, PRESET_MENU_INDEX_36H = 7, - PRESET_MENU_ITEM_COUNT = 8 + PRESET_MENU_INDEX_10S = 8, /* dev/test: fires alarm after 10 s */ + PRESET_MENU_ITEM_COUNT = 9 }; static Window *s_menu_window; @@ -569,6 +570,11 @@ static void refresh_timer_view_layers(void) { static void refresh_timer_view_idle(void) { apply_timer_theme(false); + /* Restore the large number font in case we were in overtime mode before. */ + if (s_timer_layer) { + text_layer_set_font(s_timer_layer, + fonts_get_system_font(FONT_KEY_BITHAM_34_MEDIUM_NUMBERS)); + } snprintf(s_title_text, sizeof(s_title_text), "NO FAST RUNNING"); format_hhmmss(0, s_timer_text, sizeof(s_timer_text)); snprintf(s_detail_text, sizeof(s_detail_text), "Target: %um S:%u/%u", @@ -588,9 +594,16 @@ static void refresh_timer_view_running(time_t elapsed) { if (target_seconds > 0) { time_t remaining = (time_t)target_seconds - elapsed; if (remaining > 0) { + /* Positive countdown: use the large number font — "HH:MM:SS" = 8 chars fits fine. */ + text_layer_set_font(s_timer_layer, + fonts_get_system_font(FONT_KEY_BITHAM_34_MEDIUM_NUMBERS)); snprintf(s_title_text, sizeof(s_title_text), "COUNTDOWN"); format_remaining_with_overtime(remaining, s_timer_text, sizeof(s_timer_text)); } else { + /* Overtime: "-HH:MM:SS" is 9 chars which overflows BITHAM_34 on 144 px wide display. + * Switch to GOTHIC_28_BOLD which fits all 9 characters comfortably. */ + text_layer_set_font(s_timer_layer, + fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD)); snprintf(s_title_text, sizeof(s_title_text), "GOAL REACHED"); format_remaining_with_overtime(remaining, s_timer_text, sizeof(s_timer_text)); } @@ -1155,6 +1168,42 @@ static void menu_about_callback(int index, void *context) { "BACK Menu"); } +/* Start a fast whose alarm fires after 10 seconds, used for quick dev/test + * runs of the goal-reached flow. The stored target_minutes stays 1 (the + * SDK minimum) so the data model remains consistent; only the live alarm + * timer is shortened via fastforge_reschedule_alarm_for_seconds(). */ +static void start_fast_from_test_preset(void) { + if (fast_is_running()) { + show_placeholder_window("FAST RUNNING", + "Stop the current fast before starting a new one.", + "BACK Menu"); + return; + } + + global_target_minutes = 1; + if (!fast_start(1)) { + show_placeholder_window("FAST RUNNING", + "Stop the current fast before starting a new one.", + "BACK Menu"); + return; + } + + /* Shorten the alarm to 10 s (fast_start registered a 60 s one). */ + fastforge_reschedule_alarm_for_seconds(10); + + if (window_stack_contains_window(s_presets_window)) { + window_stack_remove(s_presets_window, false); + } + safe_push_window(s_timer_window, true); + refresh_all_ui_state(); +} + +static void preset_10s_callback(int index, void *context) { + (void)index; + (void)context; + start_fast_from_test_preset(); +} + static void preset_16h_callback(int index, void *context) { (void)index; (void)context; @@ -1329,6 +1378,7 @@ static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { history_menu_reload(); } refresh_timer_view(); + refresh_goal_window_content(); /* keep elapsed time live while goal window is open */ refresh_running_edit_window_content(); } @@ -1437,7 +1487,8 @@ static void goal_window_load(Window *window) { GTextAlignmentCenter, FONT_KEY_GOTHIC_14_BOLD, theme_goal_text_color(), theme_goal_background_color(), true); - text_layer_set_text(s_goal_hint_layer, "SELECT Stop\nDOWN Continue"); + /* Fast continues automatically; any key dismisses this overlay. */ + text_layer_set_text(s_goal_hint_layer, "BACK/DN Dismiss\nSEL Stop fast"); add_text_layer(window_layer, s_goal_title_layer); add_text_layer(window_layer, s_goal_time_layer); @@ -1856,6 +1907,13 @@ static void configure_preset_items(void) { .subtitle = "Deep ketosis push", .callback = preset_36h_callback }; + /* Dev/test preset: alarm fires after 10 s so the goal-reached flow can be + * exercised quickly without waiting hours. Kept permanently as last item. */ + s_presets_menu_items[PRESET_MENU_INDEX_10S] = (SimpleMenuItem) { + .title = "10 seconds", + .subtitle = "Dev: test goal alarm", + .callback = preset_10s_callback + }; s_presets_menu_sections[0] = (SimpleMenuSection) { .title = "Start New Fast", diff --git a/src/c/fastforge_core.c b/src/c/fastforge_core.c index 0b4455e..605ea3b 100644 --- a/src/c/fastforge_core.c +++ b/src/c/fastforge_core.c @@ -350,3 +350,15 @@ static void alarm_callback(void *data) { void fastforge_force_goal_alarm(void) { alarm_callback(NULL); } + +/* Override the scheduled alarm to fire after the given number of seconds. + * Used by the 10-second dev preset to bypass the per-minute granularity of + * target_minutes without touching the FastEntry data model. */ +void fastforge_reschedule_alarm_for_seconds(uint32_t seconds) { + if (alarm_timer) { + app_timer_cancel(alarm_timer); + alarm_timer = NULL; + } + target_time = fastforge_now() + (time_t)seconds; + alarm_timer = app_timer_register(seconds * 1000, alarm_callback, NULL); +} diff --git a/src/c/fastforge_internal.h b/src/c/fastforge_internal.h index 980c00d..4fabda8 100644 --- a/src/c/fastforge_internal.h +++ b/src/c/fastforge_internal.h @@ -35,5 +35,6 @@ void show_placeholder_window(const char *title, const char *body, const char *hi void fastforge_history_register_app_message_handlers(void); void fastforge_history_stop_export(void); void fastforge_force_goal_alarm(void); +void fastforge_reschedule_alarm_for_seconds(uint32_t seconds); #endif diff --git a/src/c/fastforge_logic.c b/src/c/fastforge_logic.c index 3233c00..de93a5c 100644 --- a/src/c/fastforge_logic.c +++ b/src/c/fastforge_logic.c @@ -9,7 +9,6 @@ typedef time_t ff_sys_time_t; #define time_t long #include <stdio.h> -#include <stdlib.h> #include <stdint.h> @@ -84,38 +83,21 @@ time_t local_day_start(time_t timestamp) { return (time_t)mktime(&tm_copy); } -static int compare_time_t_ascending(const void *a, const void *b) { - const time_t time_a = *(const time_t *)a; - const time_t time_b = *(const time_t *)b; - if (time_a < time_b) { - return -1; - } - if (time_a > time_b) { - return 1; - } - return 0; -} - -static bool is_next_local_day(time_t first_day, time_t second_day) { - if (first_day <= 0 || second_day <= 0 || second_day < first_day) { - return false; - } - ff_sys_time_t sys_first_day = (ff_sys_time_t)first_day; - struct tm *tm_info = localtime(&sys_first_day); - if (!tm_info) { - return false; - } - struct tm next_day_tm = *tm_info; - next_day_tm.tm_mday += 1; - next_day_tm.tm_hour = 0; - next_day_tm.tm_min = 0; - next_day_tm.tm_sec = 0; - next_day_tm.tm_isdst = -1; - return (time_t)mktime(&next_day_tm) == second_day; +/* Convert a Unix timestamp to a UTC day number (seconds since epoch / 86400). + * Avoids localtime()/mktime() calls that consume significant stack depth. */ +static time_t utc_day_number(time_t t) { + return (t > 0) ? (t / 86400) : -1; } +/* Recompute streak data from a history array sorted by end_time ascending + * (guaranteed by sort_history_by_end_time()). + * + * Uses UTC day numbers via integer division to avoid localtime()/mktime() + * calls — those functions burn stack in the already-deep Pebble call chain + * (app task stack ≈2 KB, typically 1.5 KB consumed by firmware before our + * click handler fires). */ void fastforge_streak_recompute(const FastEntry *entries, int count, time_t now, StreakData *out) { if (!out) { return; @@ -129,21 +111,13 @@ void fastforge_streak_recompute(const FastEntry *entries, int count, time_t now, return; } - if ((size_t)count > SIZE_MAX / sizeof(time_t)) { - return; - } - - size_t completion_capacity = (size_t)count; - time_t *completion_days = malloc(completion_capacity * sizeof(*completion_days)); - if (!completion_days) { - return; - } - int completion_day_count = 0; + time_t prev_day = -1; + uint16_t run_length = 0; + uint16_t longest = 0; for (int i = 0; i < count; i++) { const FastEntry *entry = &entries[i]; - time_t duration = entry_duration_seconds(entry); - if (duration <= 0 || entry->end_time <= 0) { + if (entry_duration_seconds(entry) <= 0 || entry->end_time <= 0) { continue; } @@ -151,32 +125,17 @@ void fastforge_streak_recompute(const FastEntry *entries, int count, time_t now, out->last_completed_fast_end = entry->end_time; } - time_t day_start = local_day_start(entry->end_time); - if (day_start <= 0) { + time_t day = utc_day_number(entry->end_time); + if (day < 0) { continue; } - completion_days[completion_day_count++] = day_start; - } - - if (completion_day_count <= 0) { - free(completion_days); - return; - } - - qsort(completion_days, (size_t)completion_day_count, sizeof(time_t), compare_time_t_ascending); - - uint16_t run_length = 0; - uint16_t longest = 0; - for (int i = 0; i < completion_day_count; i++) { - if (i == 0 || completion_days[i] == completion_days[i - 1]) { - if (i == 0) { - run_length = 1; - } + if (day == prev_day) { + /* Multiple fasts on the same UTC day — count the day only once. */ continue; } - if (is_next_local_day(completion_days[i - 1], completion_days[i])) { + if (prev_day < 0 || day == prev_day + 1) { run_length++; } else { run_length = 1; @@ -185,20 +144,23 @@ void fastforge_streak_recompute(const FastEntry *entries, int count, time_t now, if (run_length > longest) { longest = run_length; } + prev_day = day; + } + + if (run_length == 0) { + return; /* no valid completions */ } if (longest == 0) { - longest = 1; + longest = run_length; } - time_t today_day_start = local_day_start(now); - time_t last_completion_day = completion_days[completion_day_count - 1]; - if (last_completion_day == today_day_start || - is_next_local_day(last_completion_day, today_day_start)) { + /* Current streak is live only if the last completion was today or yesterday. */ + time_t today = utc_day_number(now); + if (prev_day == today || prev_day == today - 1) { out->current_streak = run_length; } out->longest_streak = longest; - free(completion_days); } bool running_fast_is_at_target(const FastEntry *entry, time_t now) { |
