|
|
|
|
@@ -1,6 +1,7 @@
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// Nine Hazes — watchface (v0.2, Idle Task Protocol)
|
|
|
|
|
// Replace the WHOLE file with this. Do not splice.
|
|
|
|
|
// Nine Hazes — watchface (Idle Task Protocol v1.0, GameSnapshot layer)
|
|
|
|
|
// Compatible with: snapshot.h (GameSnapshot), widgets.h (10 types, LAYOUT_VERSION 1)
|
|
|
|
|
// package.json messageKeys incl. requestSync
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
#include <pebble.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
@@ -12,9 +13,12 @@
|
|
|
|
|
static Window *s_window;
|
|
|
|
|
static Layer *s_canvas;
|
|
|
|
|
static LayoutState s_layout;
|
|
|
|
|
|
|
|
|
|
static char s_clock[8], s_date[16], s_strip[64];
|
|
|
|
|
static char s_steps[16], s_dist[16], s_sleep[16], s_hrm[12];
|
|
|
|
|
static bool s_health_ok = false;
|
|
|
|
|
static int s_bat_pct = -1;
|
|
|
|
|
static bool s_server_down = false; // volatile: set on failed check, cleared by absence
|
|
|
|
|
static bool s_server_down = false; // set by serverDown tuple, cleared on good sync
|
|
|
|
|
|
|
|
|
|
// ---- battery estimator ------------------------------------------------------
|
|
|
|
|
#define BAT_SAMPLES_MAX 16
|
|
|
|
|
@@ -41,7 +45,9 @@ static void bat_sample(uint8_t pct) {
|
|
|
|
|
persist_write_data(BAT_KEY, &s_batlog, sizeof(s_batlog));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int bat_days_remaining(void) {
|
|
|
|
|
// Estimated hours until the 5% power-save floor. Returns -1 if insufficient
|
|
|
|
|
// data. Note: usable capacity = pct - 5 (watch enters extreme power-save at 5%).
|
|
|
|
|
static double bat_hours_remaining(void) {
|
|
|
|
|
if (s_batlog.count < 3) return -1;
|
|
|
|
|
int n = s_batlog.count;
|
|
|
|
|
double sx = 0, sy = 0, sxy = 0, sxx = 0;
|
|
|
|
|
@@ -52,9 +58,11 @@ static int bat_days_remaining(void) {
|
|
|
|
|
}
|
|
|
|
|
double denom = n * sxx - sx * sx;
|
|
|
|
|
if (denom <= 0) return -1;
|
|
|
|
|
double slope = (n * sxy - sx * sy) / denom;
|
|
|
|
|
double slope = (n * sxy - sx * sy) / denom; // %/day
|
|
|
|
|
if (slope >= -0.05) return -1;
|
|
|
|
|
return (int)((double)s_batlog.pct[n - 1] / -slope + 0.5);
|
|
|
|
|
double usable = (double)s_batlog.pct[n - 1] - 5.0;
|
|
|
|
|
if (usable < 0) usable = 0;
|
|
|
|
|
return usable / -slope * 24.0; // hours
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- palette & fonts ---------------------------------------------------------
|
|
|
|
|
@@ -79,6 +87,39 @@ static void fonts_load(void) {
|
|
|
|
|
s_fonts[4] = fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void health_refresh(void) {
|
|
|
|
|
HealthServiceAccessibilityMask acc =
|
|
|
|
|
health_service_metric_accessible(HealthMetricStepCount,
|
|
|
|
|
time_start_of_today(), time(NULL));
|
|
|
|
|
s_health_ok = (acc == HealthServiceAccessibilityMaskAvailable);
|
|
|
|
|
if (!s_health_ok) {
|
|
|
|
|
snprintf(s_steps, sizeof(s_steps), "--");
|
|
|
|
|
snprintf(s_dist, sizeof(s_dist), "--");
|
|
|
|
|
snprintf(s_sleep, sizeof(s_sleep), "--");
|
|
|
|
|
snprintf(s_hrm, sizeof(s_hrm), "--");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int steps = (int)health_service_sum_today(HealthMetricStepCount);
|
|
|
|
|
snprintf(s_steps, sizeof(s_steps), "%d", steps);
|
|
|
|
|
|
|
|
|
|
int meters = (int)health_service_sum_today(HealthMetricWalkedDistanceMeters);
|
|
|
|
|
if (meters >= 1000) snprintf(s_dist, sizeof(s_dist), "%d.%01dkm",
|
|
|
|
|
meters / 1000, (meters % 1000) / 100);
|
|
|
|
|
else snprintf(s_dist, sizeof(s_dist), "%dm", meters);
|
|
|
|
|
|
|
|
|
|
int sleep_s = (int)health_service_sum_today(HealthMetricSleepSeconds);
|
|
|
|
|
snprintf(s_sleep, sizeof(s_sleep), "%dh%02dm", sleep_s / 3600,
|
|
|
|
|
(sleep_s % 3600) / 60);
|
|
|
|
|
|
|
|
|
|
// Heart rate: 1-minute average; 0 = no current sensor reading
|
|
|
|
|
int bpm = (int)health_service_sum_averaged(HealthMetricHeartRateBPM,
|
|
|
|
|
time(NULL) - 60, time(NULL),
|
|
|
|
|
HealthServiceTimeScopeOnce);
|
|
|
|
|
if (bpm > 0) snprintf(s_hrm, sizeof(s_hrm), "%d", bpm);
|
|
|
|
|
else snprintf(s_hrm, sizeof(s_hrm), "--");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- values --------------------------------------------------------------------
|
|
|
|
|
static void update_values(void) {
|
|
|
|
|
time_t now = time(NULL);
|
|
|
|
|
@@ -101,7 +142,8 @@ static void update_values(void) {
|
|
|
|
|
snprintf(s_strip, sizeof(s_strip), "done %u/36 seg\n%s (collect in game)",
|
|
|
|
|
s->used, taskname_get());
|
|
|
|
|
}
|
|
|
|
|
} else if (s->status == 2) snprintf(s_strip, sizeof(s_strip), "FROZEN — in play");
|
|
|
|
|
}
|
|
|
|
|
else if (s->status == 2) snprintf(s_strip, sizeof(s_strip), "FROZEN - in play");
|
|
|
|
|
else if (s->status == 3) snprintf(s_strip, sizeof(s_strip), "ABORTED (%d)", s->abort_code);
|
|
|
|
|
else if (s->status == 4) snprintf(s_strip, sizeof(s_strip), "LOCKED");
|
|
|
|
|
else snprintf(s_strip, sizeof(s_strip), "No hero yet");
|
|
|
|
|
@@ -121,38 +163,83 @@ static void canvas_update(Layer *layer, GContext *ctx) {
|
|
|
|
|
|
|
|
|
|
const char *text;
|
|
|
|
|
char buf[64];
|
|
|
|
|
int16_t tx_off = 0; // text x-offset: heart icon leads its number
|
|
|
|
|
switch ((WidgetType)w->type) {
|
|
|
|
|
case W_TIME: text = s_clock; break;
|
|
|
|
|
case W_DATE: text = s_date; break;
|
|
|
|
|
case W_BATTERY: {
|
|
|
|
|
int days = bat_days_remaining();
|
|
|
|
|
if (days < 0) snprintf(buf, sizeof(buf), "%d%%", s_bat_pct);
|
|
|
|
|
else snprintf(buf, sizeof(buf), "%d%% (~%dd)", s_bat_pct, days);
|
|
|
|
|
text = buf; break;
|
|
|
|
|
double hrs = bat_hours_remaining();
|
|
|
|
|
if (hrs < 0) {
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d%%", s_bat_pct);
|
|
|
|
|
} else if (hrs >= 24) {
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d%% (~%dd)", s_bat_pct,
|
|
|
|
|
(int)(hrs / 24.0 + 0.5));
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d%% (~%dh)", s_bat_pct,
|
|
|
|
|
(int)(hrs + 0.5));
|
|
|
|
|
}
|
|
|
|
|
text = buf;
|
|
|
|
|
graphics_context_set_text_color(ctx, pal_color(PAL_ACCENT));
|
|
|
|
|
graphics_draw_text(ctx, "WB", s_fonts[1],
|
|
|
|
|
GRect(w->x, w->y - 4, 24, 26),
|
|
|
|
|
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
|
|
|
|
|
tx_off = 26;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case W_PHONE_BATTERY: text = "phone?"; break;
|
|
|
|
|
case W_STEPS: text = "steps?"; break;
|
|
|
|
|
case W_DISTANCE: text = "dist?"; break;
|
|
|
|
|
case W_WEATHER: text = "--/--"; break;
|
|
|
|
|
case W_OMEN: text = ""; break;
|
|
|
|
|
case W_SLEEP: text = "sleep?"; break;
|
|
|
|
|
case W_TASK_STRIP: text = s_strip; break;
|
|
|
|
|
case W_PHONE_BATTERY: text = "phone?"; break;
|
|
|
|
|
case W_STEPS: {
|
|
|
|
|
graphics_context_set_text_color(ctx, pal_color(PAL_POSITIVE));
|
|
|
|
|
graphics_draw_text(ctx, "S", s_fonts[1],
|
|
|
|
|
GRect(w->x, w->y - 4, 14, 26),
|
|
|
|
|
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
|
|
|
|
|
text = s_steps;
|
|
|
|
|
tx_off = 16;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case W_DISTANCE: {
|
|
|
|
|
graphics_context_set_text_color(ctx, pal_color(PAL_POSITIVE));
|
|
|
|
|
graphics_draw_text(ctx, "D", s_fonts[1],
|
|
|
|
|
GRect(w->x, w->y - 4, 14, 26),
|
|
|
|
|
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
|
|
|
|
|
text = s_dist;
|
|
|
|
|
tx_off = 16;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case W_SLEEP: text = s_sleep; break;
|
|
|
|
|
case W_WEATHER: text = "--/--"; break;
|
|
|
|
|
case W_OMEN: text = ""; break;
|
|
|
|
|
case W_HEART: {
|
|
|
|
|
// state-indicating marker: moss = reading, oxblood = no signal
|
|
|
|
|
int reading = (strcmp(s_hrm, "--") != 0);
|
|
|
|
|
graphics_context_set_text_color(ctx,
|
|
|
|
|
reading ? pal_color(PAL_POSITIVE) : pal_color(PAL_WARNING));
|
|
|
|
|
graphics_draw_text(ctx, "H", s_fonts[1],
|
|
|
|
|
GRect(w->x, w->y - 4, 14, 26),
|
|
|
|
|
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
|
|
|
|
|
snprintf(buf, sizeof(buf), "%s bpm", s_hrm);
|
|
|
|
|
text = buf;
|
|
|
|
|
tx_off = 16;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case W_TASK_STRIP: text = s_strip; break;
|
|
|
|
|
default: continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GFont f = (w->font_idx < 5) ? s_fonts[w->font_idx] : s_fonts[1];
|
|
|
|
|
graphics_context_set_text_color(ctx, pal_color((PalIndex)w->color_idx));
|
|
|
|
|
int16_t h = (w->type == W_TASK_STRIP) ? 56 : 40; // two-line strip
|
|
|
|
|
GRect box = GRect(w->x, w->y, b.size.w - w->x, h);
|
|
|
|
|
GRect box = GRect(w->x + tx_off, w->y, b.size.w - w->x - tx_off, h);
|
|
|
|
|
graphics_draw_text(ctx, text, f, box, GTextOverflowModeTrailingEllipsis,
|
|
|
|
|
GTextAlignmentLeft, NULL);
|
|
|
|
|
|
|
|
|
|
// offline flag: oxblood "!" at strip's top-right
|
|
|
|
|
// offline flag: oxblood "!" at strip's top-right, part of the game
|
|
|
|
|
// widget render pass — hidden when the task strip is disabled/invisible.
|
|
|
|
|
// ASCII placeholder; becomes a PDC sprite later.
|
|
|
|
|
if (s_server_down && w->type == W_TASK_STRIP) {
|
|
|
|
|
graphics_context_set_text_color(ctx, pal_color(PAL_WARNING));
|
|
|
|
|
graphics_draw_text(ctx, "!", s_fonts[3],
|
|
|
|
|
GRect(b.size.w - 20, w->y - 4, 20, 28),
|
|
|
|
|
GTextOverflowModeWordWrap, GTextAlignmentRight, NULL);
|
|
|
|
|
GRect(b.size.w - 22, w->y - 8, 22, 36),
|
|
|
|
|
GTextOverflowModeTrailingEllipsis, GTextAlignmentRight, NULL);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -168,15 +255,16 @@ static void layout_default(void) {
|
|
|
|
|
s_layout.count = WIDGET_TYPE_COUNT;
|
|
|
|
|
const WidgetCfg defs[WIDGET_TYPE_COUNT] = {
|
|
|
|
|
{W_TIME, 1, 0, 10, 3, PAL_TEXT, 0},
|
|
|
|
|
{W_DATE, 1, 4, 70, 0, PAL_ACCENT, 0},
|
|
|
|
|
{W_BATTERY, 1, 4, 90, 0, PAL_TEXT, BAT_BOTH},
|
|
|
|
|
{W_STEPS, 1, 4, 110, 0, PAL_TEXT, 0},
|
|
|
|
|
{W_TASK_STRIP, 1, 0, 200, 1, PAL_ACCENT, 0},
|
|
|
|
|
{W_PHONE_BATTERY, 0, 4, 130, 0, PAL_TEXT, 0},
|
|
|
|
|
{W_DISTANCE, 0, 4, 150, 0, PAL_TEXT, 0},
|
|
|
|
|
{W_WEATHER, 0, 4, 130, 2, PAL_TEXT, 0},
|
|
|
|
|
{W_OMEN, 0, 4, 150, 0, PAL_POSITIVE, 0},
|
|
|
|
|
{W_SLEEP, 0, 4, 170, 1, PAL_WATER, 0},
|
|
|
|
|
{W_DATE, 1, 4, 70, 0, PAL_ACCENT, 0},
|
|
|
|
|
{W_BATTERY, 1, 4, 90, 0, PAL_TEXT, BAT_BOTH},
|
|
|
|
|
{W_PHONE_BATTERY, 0, 4, 130, 0, PAL_TEXT, 0},
|
|
|
|
|
{W_STEPS, 1, 4, 115, 0, PAL_TEXT, 0},
|
|
|
|
|
{W_DISTANCE, 1, 64,115, 0, PAL_TEXT, 0},
|
|
|
|
|
{W_HEART, 1, 122, 115, 0, PAL_TEXT, 0},
|
|
|
|
|
{W_WEATHER, 0, 4, 130, 2, PAL_TEXT, 0},
|
|
|
|
|
{W_OMEN, 0, 4, 150, 0, PAL_POSITIVE, 0},
|
|
|
|
|
{W_SLEEP, 0, 4, 170, 1, PAL_WATER, 0},
|
|
|
|
|
{W_TASK_STRIP, 1, 0, 172, 1, PAL_ACCENT, 0},
|
|
|
|
|
};
|
|
|
|
|
memcpy(s_layout.widgets, defs, sizeof(defs));
|
|
|
|
|
persist_write_data(LAYOUT_KEY, &s_layout, sizeof(s_layout));
|
|
|
|
|
@@ -195,6 +283,7 @@ static void layout_init(void) {
|
|
|
|
|
|
|
|
|
|
// ---- services --------------------------------------------------------------------
|
|
|
|
|
static void tick_handler(struct tm *t, TimeUnits u) {
|
|
|
|
|
(void)t; (void)u;
|
|
|
|
|
request_redraw();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -204,31 +293,42 @@ static void battery_handler(BatteryChargeState state) {
|
|
|
|
|
request_redraw();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void health_handler(HealthEventType event, void *context) {
|
|
|
|
|
(void)context;
|
|
|
|
|
if (event == HealthEventMovementUpdate ||
|
|
|
|
|
event == HealthEventSleepUpdate ||
|
|
|
|
|
event == HealthEventHeartRateUpdate) {
|
|
|
|
|
health_refresh();
|
|
|
|
|
request_redraw();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- AppMessage --------------------------------------------------------------------
|
|
|
|
|
static void inbox_received(DictionaryIterator *iter, void *ctx) {
|
|
|
|
|
(void)ctx;
|
|
|
|
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] inbox received");
|
|
|
|
|
Tuple *t;
|
|
|
|
|
|
|
|
|
|
// serverDown: presence means down; absence on a successful sync clears it
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_serverDown))) {
|
|
|
|
|
s_server_down = t->value->uint8 != 0;
|
|
|
|
|
s_server_down = (t->value->uint8 != 0);
|
|
|
|
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] serverDown=%d", (int)t->value->uint8);
|
|
|
|
|
request_redraw();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameSnapshot s = *snapshot_get();
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_syncTime))) s.sync_time = t->value->uint32;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_clockOff))) s.clock_off = (int32_t)t->value->int32;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_boundSec))) s.bound_sec = t->value->uint32;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_used))) s.used = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_taskCount))) s.task_count = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_curType))) s.cur_type = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_curRem))) s.cur_rem = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_status))) s.status = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_syncTime))) s.sync_time = t->value->uint32;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_clockOff))) s.clock_off = (int32_t)t->value->int32;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_boundSec))) s.bound_sec = t->value->uint32;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_used))) s.used = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_taskCount))) s.task_count = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_curType))) s.cur_type = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_curRem))) s.cur_rem = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_status))) s.status = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_abortCode))) s.abort_code = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_pendItems))) s.pend_items = t->value->uint16;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_pendXp))) s.pend_xp = t->value->uint16;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_dictV))) s.dict_v = t->value->uint8;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_pendItems))) s.pend_items = t->value->uint16;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_pendXp))) s.pend_xp = t->value->uint16;
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_dictV))) s.dict_v = t->value->uint8;
|
|
|
|
|
snapshot_store(&s);
|
|
|
|
|
|
|
|
|
|
if ((t = dict_find(iter, MESSAGE_KEY_taskName))) {
|
|
|
|
|
@@ -238,10 +338,36 @@ static void inbox_received(DictionaryIterator *iter, void *ctx) {
|
|
|
|
|
taskname_store(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_server_down = false;
|
|
|
|
|
s_server_down = false; // a good sync clears the flag
|
|
|
|
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] snapshot ok: st=%u used=%u rem=%u",
|
|
|
|
|
s.status, s.used, s.cur_rem);
|
|
|
|
|
request_redraw();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void inbox_dropped(AppMessageResult reason, void *ctx) {
|
|
|
|
|
(void)ctx;
|
|
|
|
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] inbox DROPPED: %d", (int)reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void outbox_failed(DictionaryIterator *iter, AppMessageResult reason, void *ctx) {
|
|
|
|
|
(void)iter; (void)ctx;
|
|
|
|
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] outbox FAILED: %d", (int)reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deferred send: outbox isn't reliably usable the instant after app_message_open
|
|
|
|
|
static void send_sync_request(void *data) {
|
|
|
|
|
(void)data;
|
|
|
|
|
DictionaryIterator *out;
|
|
|
|
|
AppMessageResult r = app_message_outbox_begin(&out);
|
|
|
|
|
if (r != APP_MSG_OK) {
|
|
|
|
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[INIT] outbox_begin failed: %d", (int)r);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
dict_write_uint8(out, MESSAGE_KEY_requestSync, 1);
|
|
|
|
|
r = app_message_outbox_send();
|
|
|
|
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[INIT] requestSync sent: %d", (int)r);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- window lifecycle ----------------------------------------------------------------
|
|
|
|
|
static void window_load(Window *w) {
|
|
|
|
|
Layer *root = window_get_root_layer(w);
|
|
|
|
|
@@ -254,41 +380,59 @@ static void window_load(Window *w) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void window_unload(Window *w) {
|
|
|
|
|
(void)w;
|
|
|
|
|
layer_destroy(s_canvas);
|
|
|
|
|
s_canvas = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- app lifecycle ----------------------------------------------------------------------
|
|
|
|
|
static void init(void) {
|
|
|
|
|
persist_delete(LAYOUT_KEY); // widget layout → forced back to defaults Delete
|
|
|
|
|
// synthetic drain: ~6%/day over 4 days, oldest sample first
|
|
|
|
|
memset(&s_batlog, 0, sizeof(s_batlog));
|
|
|
|
|
s_batlog.version = 1;
|
|
|
|
|
uint32_t now_min = (uint32_t)(time(NULL) / 60);
|
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
|
|
|
s_batlog.ts[i] = now_min - (uint32_t)(4 - i) * 1440; // 1 day apart
|
|
|
|
|
s_batlog.pct[i] = 100 - i * 6; // 100,94,88,82,76
|
|
|
|
|
}
|
|
|
|
|
s_batlog.count = 5;
|
|
|
|
|
persist_write_data(BAT_KEY, &s_batlog, sizeof(s_batlog));
|
|
|
|
|
fonts_load();
|
|
|
|
|
layout_init();
|
|
|
|
|
snapshot_init();
|
|
|
|
|
|
|
|
|
|
if (persist_exists(BAT_KEY) &&
|
|
|
|
|
if (!(persist_exists(BAT_KEY) &&
|
|
|
|
|
persist_read_data(BAT_KEY, &s_batlog, sizeof(s_batlog)) == (int)sizeof(s_batlog) &&
|
|
|
|
|
s_batlog.version == 1) {
|
|
|
|
|
// restored
|
|
|
|
|
} else {
|
|
|
|
|
memset(&s_batlog, 0, sizeof(s_batlog));
|
|
|
|
|
s_batlog.version = 1;
|
|
|
|
|
s_batlog.version == 1)) {
|
|
|
|
|
memset(&s_batlog, 0, sizeof(s_batlog));
|
|
|
|
|
s_batlog.version = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_window = window_create();
|
|
|
|
|
window_set_window_handlers(s_window, (WindowHandlers){
|
|
|
|
|
.load = window_load, .unload = window_unload });
|
|
|
|
|
window_stack_push(s_window, true);
|
|
|
|
|
window_set_window_handlers(s_window, (WindowHandlers){
|
|
|
|
|
.load = window_load, .unload = window_unload });
|
|
|
|
|
window_stack_push(s_window, true);
|
|
|
|
|
|
|
|
|
|
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
|
|
|
|
|
battery_state_service_subscribe(battery_handler);
|
|
|
|
|
battery_handler(battery_state_service_peek());
|
|
|
|
|
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
|
|
|
|
|
battery_state_service_subscribe(battery_handler);
|
|
|
|
|
battery_handler(battery_state_service_peek());
|
|
|
|
|
|
|
|
|
|
app_message_register_inbox_received(inbox_received);
|
|
|
|
|
app_message_open(256, 64);
|
|
|
|
|
health_refresh();
|
|
|
|
|
health_service_events_subscribe(health_handler, NULL);
|
|
|
|
|
|
|
|
|
|
app_message_register_inbox_received(inbox_received);
|
|
|
|
|
app_message_register_inbox_dropped(inbox_dropped);
|
|
|
|
|
app_message_register_outbox_failed(outbox_failed);
|
|
|
|
|
app_message_open(256, 64);
|
|
|
|
|
|
|
|
|
|
app_timer_register(300, send_sync_request, NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void deinit(void) {
|
|
|
|
|
tick_timer_service_unsubscribe();
|
|
|
|
|
battery_state_service_unsubscribe();
|
|
|
|
|
health_service_events_unsubscribe();
|
|
|
|
|
window_destroy(s_window);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|