From 6336b0ce10385692d5f54ea25acaa1065ad86b2e Mon Sep 17 00:00:00 2001 From: Mystica Venatus Date: Mon, 14 Sep 2026 06:02:21 -0400 Subject: [PATCH] feat(face): complete widget set (phone batt, sleep, omens) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phone battery via navigator.getBattery(), honest "--" when unavailable. Sleep widget enabled with M marker. Omens derived from weather condition: fog/rain/snow/thunder produce advisory text; clear shows empty. All 11 widget types rendered; layout enables all by default. Refs: DESIGN.md §5 widget set complete. --- nine-hazes-face/package.json | 3 +- nine-hazes-face/src/c/nine-hazes-face.c | 69 +++++++++++++++++++------ nine-hazes-face/src/pkjs/index.js | 24 ++++++++- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/nine-hazes-face/package.json b/nine-hazes-face/package.json index aa78d9a..e06d4a0 100644 --- a/nine-hazes-face/package.json +++ b/nine-hazes-face/package.json @@ -18,7 +18,8 @@ "messageKeys": ["taskName", "syncTime", "clockOff", "boundSec", "used", "taskCount", "curType", "curRem", "status", "abortCode", "pendItems", "pendXp", "dictV", "serverDown", "requestSync", - "weatherTemp", "weatherHi", "weatherLo", "weatherTs", "weatherCond"], + "weatherTemp", "weatherHi", "weatherLo", "weatherTs", + "weatherCond", "phoneBatt"], "resources": { "media": [] } diff --git a/nine-hazes-face/src/c/nine-hazes-face.c b/nine-hazes-face/src/c/nine-hazes-face.c index 0cc21f7..3dcb9d7 100644 --- a/nine-hazes-face/src/c/nine-hazes-face.c +++ b/nine-hazes-face/src/c/nine-hazes-face.c @@ -18,9 +18,11 @@ 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 int8_t s_phone_batt = INT8_MIN; static bool s_server_down = false; // set by serverDown tuple, cleared on good sync -static char s_weather[16]; +static char s_weather[24]; +static char s_omen[32]; static int16_t s_wx_temp = INT16_MIN, s_wx_hi = INT16_MIN, s_wx_lo = INT16_MIN; static uint8_t s_wx_cond = 0; @@ -151,12 +153,24 @@ static void update_values(void) { 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"); + + // Current Weather (rain Snow Sunnt ect..) if (s_wx_temp != INT16_MIN) { static const char wx_icon[8] = { '?', 'o', '~', 'C', 'F', 'R', '*', 'T' }; snprintf(s_weather, sizeof(s_weather), "%c %d° %d/%d", wx_icon[s_wx_cond & 7], s_wx_temp, s_wx_hi, s_wx_lo); + + // Omen derivation from weather condition + switch (s_wx_cond & 7) { + case 4: snprintf(s_omen, sizeof(s_omen), "Travel impaired"); break; // fog + case 5: snprintf(s_omen, sizeof(s_omen), "Rain delays"); break; // rain + case 6: snprintf(s_omen, sizeof(s_omen), "Cold footing"); break; // snow + case 7: snprintf(s_omen, sizeof(s_omen), "Dangerous air"); break; // thunder + default: s_omen[0] = '\0'; break;; // clear/other = no omen + } } else { snprintf(s_weather, sizeof(s_weather), "--/--"); + s_omen[0] = '\0'; } } @@ -197,7 +211,20 @@ static void canvas_update(Layer *layer, GContext *ctx) { tx_off = 26; break; } - case W_PHONE_BATTERY: text = "phone?"; break; + case W_PHONE_BATTERY: { + graphics_context_set_text_color(ctx, pal_color(PAL_ACCENT)); + graphics_draw_text(ctx, "PB", s_fonts[1], + GRect(w->x, w->y - 4, 24, 26), + GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); + if (s_phone_batt >= 0) { + snprintf(buf, sizeof(buf), "%d%%", s_phone_batt); + text = buf; + } else { + text = "--"; // no reading, honest empty + } + tx_off = 26; + break; + } case W_STEPS: { graphics_context_set_text_color(ctx, pal_color(PAL_POSITIVE)); graphics_draw_text(ctx, "S", s_fonts[1], @@ -216,9 +243,19 @@ static void canvas_update(Layer *layer, GContext *ctx) { tx_off = 16; break; } - case W_SLEEP: text = s_sleep; break; + case W_SLEEP: { + graphics_context_set_text_color(ctx, pal_color(PAL_WATER)); + graphics_draw_text(ctx, "Z", s_fonts[1], + GRect(w->x, w->y - 4, 14, 26), + GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); + text = s_sleep; + tx_off = 16; + break; + } case W_WEATHER: text = s_weather; break; - case W_OMEN: text = ""; break; + case W_OMEN: + text = s_omen[0] ? s_omen : ""; + break; case W_HEART: { // state-indicating marker: moss = reading, oxblood = no signal int reading = (strcmp(s_hrm, "--") != 0); @@ -265,17 +302,17 @@ static void layout_default(void) { s_layout.version = LAYOUT_VERSION; 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_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, 1, 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}, + {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_PHONE_BATTERY, 1, 100, 90, 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_SLEEP, 1, 4, 135, 1, PAL_WATER, 0}, + {W_WEATHER, 1, 4, 170, 2, PAL_TEXT, 0}, + {W_OMEN, 1, 4, 155, 0, PAL_POSITIVE, 0}, + {W_TASK_STRIP, 1, 0, 200, 1, PAL_ACCENT, 0}, }; memcpy(s_layout.widgets, defs, sizeof(defs)); persist_write_data(LAYOUT_KEY, &s_layout, sizeof(s_layout)); @@ -353,6 +390,8 @@ static void inbox_received(DictionaryIterator *iter, void *ctx) { if ((t = dict_find(iter, MESSAGE_KEY_weatherHi))) s_wx_hi = t->value->int16; if ((t = dict_find(iter, MESSAGE_KEY_weatherLo))) s_wx_lo = t->value->int16; + if ((t = dict_find(iter, MESSAGE_KEY_phoneBatt))) s_phone_batt = (int8_t)t->value->uint8; + 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); diff --git a/nine-hazes-face/src/pkjs/index.js b/nine-hazes-face/src/pkjs/index.js index 380863e..b4c21ed 100644 --- a/nine-hazes-face/src/pkjs/index.js +++ b/nine-hazes-face/src/pkjs/index.js @@ -92,6 +92,27 @@ function fetchWeather(cb) { }, { timeout: 8000, maximumAge: 600000 }); } +// ---- phone battery (percent only, honest-empty if unavailable) ------------- +function fetchPhoneBatt(cb) { + if (typeof navigator === 'undefined' || !navigator.getBattery) { + return cb(null); // no API / emulator + } + navigator.getBattery().then(function (b) { + var pct = Math.round(b.level * 100); + cb(pct); + }).catch(function () { cb(null); }); +} + +function sendPhoneBatt() { + fetchPhoneBatt(function (pb) { + if (pb !== null) { + Pebble.sendAppMessage({ phoneBatt: pb }, + function () { console.log('[PKJS] phone batt delivered'); }, + function (e) { console.log('[PKJS] phone batt send failed'); }); + } + }); +} + function wxLookup(lat, lon, cb) { // rough US bbox (CONUS + AK + HI + PR): NWS has data, else Open-Meteo var inUS = (lat >= 17 && lat <= 72 && lon >= -179 && lon <= -66); @@ -274,7 +295,8 @@ function sync() { Pebble.sendAppMessage({ serverDown: 1 }, function(){}, function(){}); } ); - sendWeather(); // fire-and-forget; separate message + sendWeather(); + sendPhoneBatt(); }; if (cachedV === q.dict) {