Battery remianing and HRM implemented
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"messageKeys": ["taskName", "syncTime", "clockOff", "boundSec", "used",
|
"messageKeys": ["taskName", "syncTime", "clockOff", "boundSec", "used",
|
||||||
"taskCount", "curType", "curRem", "status", "abortCode",
|
"taskCount", "curType", "curRem", "status", "abortCode",
|
||||||
"pendItems", "pendXp", "dictV", "serverDown"],
|
"pendItems", "pendXp", "dictV", "serverDown", "requestSync"],
|
||||||
"resources": {
|
"resources": {
|
||||||
"media": []
|
"media": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Nine Hazes — watchface (v0.2, Idle Task Protocol)
|
// Nine Hazes — watchface (Idle Task Protocol v1.0, GameSnapshot layer)
|
||||||
// Replace the WHOLE file with this. Do not splice.
|
// Compatible with: snapshot.h (GameSnapshot), widgets.h (10 types, LAYOUT_VERSION 1)
|
||||||
|
// package.json messageKeys incl. requestSync
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
#include <pebble.h>
|
#include <pebble.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
@@ -12,9 +13,12 @@
|
|||||||
static Window *s_window;
|
static Window *s_window;
|
||||||
static Layer *s_canvas;
|
static Layer *s_canvas;
|
||||||
static LayoutState s_layout;
|
static LayoutState s_layout;
|
||||||
|
|
||||||
static char s_clock[8], s_date[16], s_strip[64];
|
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 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 ------------------------------------------------------
|
// ---- battery estimator ------------------------------------------------------
|
||||||
#define BAT_SAMPLES_MAX 16
|
#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));
|
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;
|
if (s_batlog.count < 3) return -1;
|
||||||
int n = s_batlog.count;
|
int n = s_batlog.count;
|
||||||
double sx = 0, sy = 0, sxy = 0, sxx = 0;
|
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;
|
double denom = n * sxx - sx * sx;
|
||||||
if (denom <= 0) return -1;
|
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;
|
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 ---------------------------------------------------------
|
// ---- palette & fonts ---------------------------------------------------------
|
||||||
@@ -79,6 +87,39 @@ static void fonts_load(void) {
|
|||||||
s_fonts[4] = fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD);
|
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 --------------------------------------------------------------------
|
// ---- values --------------------------------------------------------------------
|
||||||
static void update_values(void) {
|
static void update_values(void) {
|
||||||
time_t now = time(NULL);
|
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)",
|
snprintf(s_strip, sizeof(s_strip), "done %u/36 seg\n%s (collect in game)",
|
||||||
s->used, taskname_get());
|
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 == 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 if (s->status == 4) snprintf(s_strip, sizeof(s_strip), "LOCKED");
|
||||||
else snprintf(s_strip, sizeof(s_strip), "No hero yet");
|
else snprintf(s_strip, sizeof(s_strip), "No hero yet");
|
||||||
@@ -121,21 +163,64 @@ static void canvas_update(Layer *layer, GContext *ctx) {
|
|||||||
|
|
||||||
const char *text;
|
const char *text;
|
||||||
char buf[64];
|
char buf[64];
|
||||||
|
int16_t tx_off = 0; // text x-offset: heart icon leads its number
|
||||||
switch ((WidgetType)w->type) {
|
switch ((WidgetType)w->type) {
|
||||||
case W_TIME: text = s_clock; break;
|
case W_TIME: text = s_clock; break;
|
||||||
case W_DATE: text = s_date; break;
|
case W_DATE: text = s_date; break;
|
||||||
case W_BATTERY: {
|
case W_BATTERY: {
|
||||||
int days = bat_days_remaining();
|
double hrs = bat_hours_remaining();
|
||||||
if (days < 0) snprintf(buf, sizeof(buf), "%d%%", s_bat_pct);
|
if (hrs < 0) {
|
||||||
else snprintf(buf, sizeof(buf), "%d%% (~%dd)", s_bat_pct, days);
|
snprintf(buf, sizeof(buf), "%d%%", s_bat_pct);
|
||||||
text = buf; break;
|
} 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_PHONE_BATTERY: text = "phone?"; break;
|
||||||
case W_STEPS: text = "steps?"; break;
|
case W_STEPS: {
|
||||||
case W_DISTANCE: text = "dist?"; break;
|
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_WEATHER: text = "--/--"; break;
|
||||||
case W_OMEN: text = ""; break;
|
case W_OMEN: text = ""; break;
|
||||||
case W_SLEEP: text = "sleep?"; 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;
|
case W_TASK_STRIP: text = s_strip; break;
|
||||||
default: continue;
|
default: continue;
|
||||||
}
|
}
|
||||||
@@ -143,16 +228,18 @@ static void canvas_update(Layer *layer, GContext *ctx) {
|
|||||||
GFont f = (w->font_idx < 5) ? s_fonts[w->font_idx] : s_fonts[1];
|
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));
|
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
|
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,
|
graphics_draw_text(ctx, text, f, box, GTextOverflowModeTrailingEllipsis,
|
||||||
GTextAlignmentLeft, NULL);
|
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) {
|
if (s_server_down && w->type == W_TASK_STRIP) {
|
||||||
graphics_context_set_text_color(ctx, pal_color(PAL_WARNING));
|
graphics_context_set_text_color(ctx, pal_color(PAL_WARNING));
|
||||||
graphics_draw_text(ctx, "!", s_fonts[3],
|
graphics_draw_text(ctx, "!", s_fonts[3],
|
||||||
GRect(b.size.w - 20, w->y - 4, 20, 28),
|
GRect(b.size.w - 22, w->y - 8, 22, 36),
|
||||||
GTextOverflowModeWordWrap, GTextAlignmentRight, NULL);
|
GTextOverflowModeTrailingEllipsis, GTextAlignmentRight, NULL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,13 +257,14 @@ static void layout_default(void) {
|
|||||||
{W_TIME, 1, 0, 10, 3, PAL_TEXT, 0},
|
{W_TIME, 1, 0, 10, 3, PAL_TEXT, 0},
|
||||||
{W_DATE, 1, 4, 70, 0, PAL_ACCENT, 0},
|
{W_DATE, 1, 4, 70, 0, PAL_ACCENT, 0},
|
||||||
{W_BATTERY, 1, 4, 90, 0, PAL_TEXT, BAT_BOTH},
|
{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_PHONE_BATTERY, 0, 4, 130, 0, PAL_TEXT, 0},
|
||||||
{W_DISTANCE, 0, 4, 150, 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_WEATHER, 0, 4, 130, 2, PAL_TEXT, 0},
|
||||||
{W_OMEN, 0, 4, 150, 0, PAL_POSITIVE, 0},
|
{W_OMEN, 0, 4, 150, 0, PAL_POSITIVE, 0},
|
||||||
{W_SLEEP, 0, 4, 170, 1, PAL_WATER, 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));
|
memcpy(s_layout.widgets, defs, sizeof(defs));
|
||||||
persist_write_data(LAYOUT_KEY, &s_layout, sizeof(s_layout));
|
persist_write_data(LAYOUT_KEY, &s_layout, sizeof(s_layout));
|
||||||
@@ -195,6 +283,7 @@ static void layout_init(void) {
|
|||||||
|
|
||||||
// ---- services --------------------------------------------------------------------
|
// ---- services --------------------------------------------------------------------
|
||||||
static void tick_handler(struct tm *t, TimeUnits u) {
|
static void tick_handler(struct tm *t, TimeUnits u) {
|
||||||
|
(void)t; (void)u;
|
||||||
request_redraw();
|
request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,14 +293,25 @@ static void battery_handler(BatteryChargeState state) {
|
|||||||
request_redraw();
|
request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void health_handler(HealthEventType event, void *context) {
|
||||||
|
(void)context;
|
||||||
|
if (event == HealthEventMovementUpdate ||
|
||||||
|
event == HealthEventSleepUpdate ||
|
||||||
|
event == HealthEventHeartRateUpdate) {
|
||||||
|
health_refresh();
|
||||||
|
request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- AppMessage --------------------------------------------------------------------
|
// ---- AppMessage --------------------------------------------------------------------
|
||||||
static void inbox_received(DictionaryIterator *iter, void *ctx) {
|
static void inbox_received(DictionaryIterator *iter, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] inbox received");
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] inbox received");
|
||||||
Tuple *t;
|
Tuple *t;
|
||||||
|
|
||||||
// serverDown: presence means down; absence on a successful sync clears it
|
|
||||||
if ((t = dict_find(iter, MESSAGE_KEY_serverDown))) {
|
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();
|
request_redraw();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -238,10 +338,36 @@ static void inbox_received(DictionaryIterator *iter, void *ctx) {
|
|||||||
taskname_store(name);
|
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();
|
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 ----------------------------------------------------------------
|
// ---- window lifecycle ----------------------------------------------------------------
|
||||||
static void window_load(Window *w) {
|
static void window_load(Window *w) {
|
||||||
Layer *root = window_get_root_layer(w);
|
Layer *root = window_get_root_layer(w);
|
||||||
@@ -254,21 +380,31 @@ static void window_load(Window *w) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void window_unload(Window *w) {
|
static void window_unload(Window *w) {
|
||||||
|
(void)w;
|
||||||
layer_destroy(s_canvas);
|
layer_destroy(s_canvas);
|
||||||
s_canvas = NULL;
|
s_canvas = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- app lifecycle ----------------------------------------------------------------------
|
// ---- app lifecycle ----------------------------------------------------------------------
|
||||||
static void init(void) {
|
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();
|
fonts_load();
|
||||||
layout_init();
|
layout_init();
|
||||||
snapshot_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) &&
|
persist_read_data(BAT_KEY, &s_batlog, sizeof(s_batlog)) == (int)sizeof(s_batlog) &&
|
||||||
s_batlog.version == 1) {
|
s_batlog.version == 1)) {
|
||||||
// restored
|
|
||||||
} else {
|
|
||||||
memset(&s_batlog, 0, sizeof(s_batlog));
|
memset(&s_batlog, 0, sizeof(s_batlog));
|
||||||
s_batlog.version = 1;
|
s_batlog.version = 1;
|
||||||
}
|
}
|
||||||
@@ -282,13 +418,21 @@ static void init(void) {
|
|||||||
battery_state_service_subscribe(battery_handler);
|
battery_state_service_subscribe(battery_handler);
|
||||||
battery_handler(battery_state_service_peek());
|
battery_handler(battery_state_service_peek());
|
||||||
|
|
||||||
|
health_refresh();
|
||||||
|
health_service_events_subscribe(health_handler, NULL);
|
||||||
|
|
||||||
app_message_register_inbox_received(inbox_received);
|
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_message_open(256, 64);
|
||||||
|
|
||||||
|
app_timer_register(300, send_sync_request, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void deinit(void) {
|
static void deinit(void) {
|
||||||
tick_timer_service_unsubscribe();
|
tick_timer_service_unsubscribe();
|
||||||
battery_state_service_unsubscribe();
|
battery_state_service_unsubscribe();
|
||||||
|
health_service_events_unsubscribe();
|
||||||
window_destroy(s_window);
|
window_destroy(s_window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <pebble.h>
|
#include <pebble.h>
|
||||||
|
|
||||||
// ---- Widget types (10, locked in DESIGN.md §5) ---------------------------
|
// ---- Widget types (DESIGN.md §5 + heart) -------------------------------
|
||||||
typedef enum {
|
typedef enum {
|
||||||
W_TIME = 0,
|
W_TIME = 0,
|
||||||
W_DATE,
|
W_DATE,
|
||||||
@@ -9,6 +9,7 @@ typedef enum {
|
|||||||
W_PHONE_BATTERY,
|
W_PHONE_BATTERY,
|
||||||
W_STEPS,
|
W_STEPS,
|
||||||
W_DISTANCE,
|
W_DISTANCE,
|
||||||
|
W_HEART, // heart-rate BPM, drawn icon + number
|
||||||
W_WEATHER, // composite: current temp + hi/lo (sub-elements later)
|
W_WEATHER, // composite: current temp + hi/lo (sub-elements later)
|
||||||
W_OMEN, // weather advisory flavor strip
|
W_OMEN, // weather advisory flavor strip
|
||||||
W_SLEEP,
|
W_SLEEP,
|
||||||
@@ -42,9 +43,9 @@ typedef struct __attribute__((packed)) {
|
|||||||
uint8_t aux; // per-type: BatMode for battery, sub-flags later
|
uint8_t aux; // per-type: BatMode for battery, sub-flags later
|
||||||
} WidgetCfg;
|
} WidgetCfg;
|
||||||
|
|
||||||
#define LAYOUT_VERSION 1
|
#define LAYOUT_VERSION 3
|
||||||
#define LAYOUT_KEY 100
|
#define LAYOUT_KEY 100
|
||||||
#define WIDGET_SLOTS 12 // 10 types + 2 spare, matching "12" scaffold note
|
#define WIDGET_SLOTS 12 // 11 types + 1 spare
|
||||||
|
|
||||||
typedef struct __attribute__((packed)) {
|
typedef struct __attribute__((packed)) {
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
|
|||||||
@@ -10,11 +10,19 @@ var CONFIG = {
|
|||||||
|
|
||||||
function get(path, cb) {
|
function get(path, cb) {
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
var abortTimer = setTimeout(function () { xhr.abort(); }, 5000);
|
var done = false;
|
||||||
|
var abortTimer = setTimeout(function () {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
cb('timeout');
|
||||||
|
try { xhr.abort(); } catch (e) {}
|
||||||
|
}, 5000);
|
||||||
xhr.open('GET', CONFIG.baseUrl + path, true);
|
xhr.open('GET', CONFIG.baseUrl + path, true);
|
||||||
xhr.setRequestHeader('Authorization', 'Bearer ' + CONFIG.token);
|
xhr.setRequestHeader('Authorization', 'Bearer ' + CONFIG.token);
|
||||||
|
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
clearTimeout(abortTimer);
|
clearTimeout(abortTimer);
|
||||||
if (xhr.status === 204) { return cb(null, null); }
|
if (xhr.status === 204) { return cb(null, null); }
|
||||||
if (xhr.status !== 200) { return cb('HTTP ' + xhr.status); }
|
if (xhr.status !== 200) { return cb('HTTP ' + xhr.status); }
|
||||||
@@ -22,6 +30,8 @@ function get(path, cb) {
|
|||||||
catch (e) { cb('malformed'); }
|
catch (e) { cb('malformed'); }
|
||||||
};
|
};
|
||||||
xhr.onerror = function () {
|
xhr.onerror = function () {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
clearTimeout(abortTimer);
|
clearTimeout(abortTimer);
|
||||||
cb('network');
|
cb('network');
|
||||||
};
|
};
|
||||||
@@ -60,21 +70,39 @@ Pebble.addEventListener('ready', function () {
|
|||||||
sync();
|
sync();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// In your pkjs index.js, add:
|
||||||
Pebble.addEventListener('appmessage', function(e) {
|
Pebble.addEventListener('appmessage', function(e) {
|
||||||
console.log('[PKJS] appmessage from watch: ' + JSON.stringify(e.payload));
|
console.log('[PKJS] Received appmessage from watch: ' + JSON.stringify(e.payload));
|
||||||
|
if (e.payload.requestSync === 1) {
|
||||||
|
console.log('[PKJS] Watch requested sync, triggering sync()');
|
||||||
|
sync();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Pebble.addEventListener('showConfiguration', function () {});
|
Pebble.addEventListener('showConfiguration', function () {});
|
||||||
Pebble.addEventListener('webviewclosed', function () {});
|
Pebble.addEventListener('webviewclosed', function () {});
|
||||||
|
|
||||||
function sync() {
|
function sync() {
|
||||||
|
console.log('[PKJS] Sync Function Entered');
|
||||||
|
|
||||||
get('/v1/queue', function (err, q) {
|
get('/v1/queue', function (err, q) {
|
||||||
if (err || !q) {
|
if (err || !q) {
|
||||||
console.log('[PKJS] fetch failed (' + err + ') — flagging serverDown');
|
console.log('[PKJS] fetch failed (' + err + ') — flagging serverDown');
|
||||||
Pebble.sendAppMessage({ serverDown: 1 }, function () {}, function () {});
|
|
||||||
|
// Explicit success/failure callbacks for sendAppMessage
|
||||||
|
Pebble.sendAppMessage(
|
||||||
|
{ serverDown: 1 },
|
||||||
|
function() {
|
||||||
|
console.log('[PKJS] serverDown flag delivered successfully');
|
||||||
|
},
|
||||||
|
function(e) {
|
||||||
|
console.log('[PKJS] FAILED to send serverDown: ' + JSON.stringify(e));
|
||||||
|
}
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... rest of your existing sync logic with enhanced logging
|
||||||
var msg = {
|
var msg = {
|
||||||
syncTime: Math.floor(Date.now() / 1000),
|
syncTime: Math.floor(Date.now() / 1000),
|
||||||
clockOff: q.t - Date.now(),
|
clockOff: q.t - Date.now(),
|
||||||
@@ -90,26 +118,45 @@ function sync() {
|
|||||||
dictV: q.dict,
|
dictV: q.dict,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[PKJS] Get /v1/queue - building message with ' + msg.taskCount + ' tasks');
|
||||||
|
|
||||||
var cachedV = parseInt(localStorage.getItem('dict_v') || '-1', 10);
|
var cachedV = parseInt(localStorage.getItem('dict_v') || '-1', 10);
|
||||||
var finish = function (names) {
|
var finish = function (names) {
|
||||||
if (msg.curType !== 255 && names && names[msg.curType]) {
|
if (msg.curType !== 255 && names && names[msg.curType]) {
|
||||||
var nameStr = String(names[msg.curType]).slice(0, 24);
|
var nameStr = String(names[msg.curType]).slice(0, 24);
|
||||||
msg.taskName = nameStr;
|
msg.taskName = nameStr;
|
||||||
|
console.log('[PKJS] Task name resolved: ' + nameStr);
|
||||||
}
|
}
|
||||||
Pebble.sendAppMessage(msg,
|
|
||||||
function () { console.log('[PKJS] snapshot delivered'); },
|
Pebble.sendAppMessage(
|
||||||
function (e) { console.log('[PKJS] send failed: ' + JSON.stringify(e)); });
|
msg,
|
||||||
|
function () {
|
||||||
|
console.log('[PKJS] snapshot delivered successfully');
|
||||||
|
// Clear serverDown flag on successful sync
|
||||||
|
// Note: This is implicit - absence of serverDown key means "up"
|
||||||
|
},
|
||||||
|
function (e) {
|
||||||
|
console.log('[PKJS] send failed: ' + JSON.stringify(e));
|
||||||
|
// Optionally flag server down on send failure too
|
||||||
|
Pebble.sendAppMessage({ serverDown: 1 }, function(){}, function(){});
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cachedV === q.dict) {
|
if (cachedV === q.dict) {
|
||||||
try {
|
try {
|
||||||
finish(JSON.parse(localStorage.getItem('dict_names') || '{}'));
|
var cachedNames = JSON.parse(localStorage.getItem('dict_names') || '{}');
|
||||||
|
console.log('[PKJS] Using cached dictionary v' + cachedV);
|
||||||
|
finish(cachedNames);
|
||||||
return;
|
return;
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
console.log('[PKJS] Cache parse failed, fetching fresh');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get('/v1/dictionary?v=' + cachedV, function (derr, dict) {
|
get('/v1/dictionary?v=' + cachedV, function (derr, dict) {
|
||||||
if (derr || !dict) {
|
if (derr || !dict) {
|
||||||
|
console.log('[PKJS] Dictionary fetch failed, using cache or null');
|
||||||
try {
|
try {
|
||||||
finish(JSON.parse(localStorage.getItem('dict_names') || '{}'));
|
finish(JSON.parse(localStorage.getItem('dict_names') || '{}'));
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
@@ -117,6 +164,8 @@ function sync() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[PKJS] Get /v1/dictionary?v=' + dict.v);
|
||||||
var names = {};
|
var names = {};
|
||||||
for (var i = 0; i < dict.tasks.length; i++) {
|
for (var i = 0; i < dict.tasks.length; i++) {
|
||||||
var t = dict.tasks[i];
|
var t = dict.tasks[i];
|
||||||
|
|||||||
Reference in New Issue
Block a user