summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-13 19:35:40 +0300
committerPaul Buetow <paul@buetow.org>2026-04-13 19:35:40 +0300
commit37c119c4a8e1749727958a6ced7e36ceda9ad9db (patch)
tree546072049d0bcac5d0a37eea963659e4092aafd8
parent32b7460685f61aceb51703a5727b26575aacb96d (diff)
s3: goal auto-continue, 10s test preset, fix overtime font and streak crash
- Goal window auto-continues the fast; BACK/DOWN dismisses, SELECT stops - 10s dev preset added as last menu item (fires alarm after 10 s) - Switch to GOTHIC_28_BOLD for negative overtime to prevent truncation - Rewrite streak computation with UTC day integers to fix stack overflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--src/c/fastforge.c62
-rw-r--r--src/c/fastforge_core.c12
-rw-r--r--src/c/fastforge_internal.h1
-rw-r--r--src/c/fastforge_logic.c96
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) {