feat(face): complete widget set (phone batt, sleep, omens)

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.
This commit is contained in:
Mystica Venatus
2026-09-14 06:02:21 -04:00
parent 917fdfa2f5
commit 6336b0ce10
3 changed files with 79 additions and 17 deletions

View File

@@ -18,7 +18,8 @@
"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", "requestSync", "pendItems", "pendXp", "dictV", "serverDown", "requestSync",
"weatherTemp", "weatherHi", "weatherLo", "weatherTs", "weatherCond"], "weatherTemp", "weatherHi", "weatherLo", "weatherTs",
"weatherCond", "phoneBatt"],
"resources": { "resources": {
"media": [] "media": []
} }

View File

@@ -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 char s_steps[16], s_dist[16], s_sleep[16], s_hrm[12];
static bool s_health_ok = false; static bool s_health_ok = false;
static int s_bat_pct = -1; 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 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 int16_t s_wx_temp = INT16_MIN, s_wx_hi = INT16_MIN, s_wx_lo = INT16_MIN;
static uint8_t s_wx_cond = 0; 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 == 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");
// Current Weather (rain Snow Sunnt ect..)
if (s_wx_temp != INT16_MIN) { if (s_wx_temp != INT16_MIN) {
static const char wx_icon[8] = { '?', 'o', '~', 'C', 'F', 'R', '*', 'T' }; static const char wx_icon[8] = { '?', 'o', '~', 'C', 'F', 'R', '*', 'T' };
snprintf(s_weather, sizeof(s_weather), "%c %d° %d/%d", 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); 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 { } else {
snprintf(s_weather, sizeof(s_weather), "--/--"); 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; tx_off = 26;
break; 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: { case W_STEPS: {
graphics_context_set_text_color(ctx, pal_color(PAL_POSITIVE)); graphics_context_set_text_color(ctx, pal_color(PAL_POSITIVE));
graphics_draw_text(ctx, "S", s_fonts[1], graphics_draw_text(ctx, "S", s_fonts[1],
@@ -216,9 +243,19 @@ static void canvas_update(Layer *layer, GContext *ctx) {
tx_off = 16; tx_off = 16;
break; 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_WEATHER: text = s_weather; break;
case W_OMEN: text = ""; break; case W_OMEN:
text = s_omen[0] ? s_omen : "";
break;
case W_HEART: { case W_HEART: {
// state-indicating marker: moss = reading, oxblood = no signal // state-indicating marker: moss = reading, oxblood = no signal
int reading = (strcmp(s_hrm, "--") != 0); int reading = (strcmp(s_hrm, "--") != 0);
@@ -265,17 +302,17 @@ static void layout_default(void) {
s_layout.version = LAYOUT_VERSION; s_layout.version = LAYOUT_VERSION;
s_layout.count = WIDGET_TYPE_COUNT; s_layout.count = WIDGET_TYPE_COUNT;
const WidgetCfg defs[WIDGET_TYPE_COUNT] = { const WidgetCfg defs[WIDGET_TYPE_COUNT] = {
{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_PHONE_BATTERY, 0, 4, 130, 0, PAL_TEXT, 0}, {W_PHONE_BATTERY, 1, 100, 90, 0, PAL_TEXT, 0},
{W_STEPS, 1, 4, 115, 0, PAL_TEXT, 0}, {W_STEPS, 1, 4, 115, 0, PAL_TEXT, 0},
{W_DISTANCE, 1, 64,115, 0, PAL_TEXT, 0}, {W_DISTANCE, 1, 64, 115, 0, PAL_TEXT, 0},
{W_HEART, 1, 122, 115, 0, PAL_TEXT, 0}, {W_HEART, 1, 122, 115, 0, PAL_TEXT, 0},
{W_WEATHER, 1, 4, 130, 2, PAL_TEXT, 0}, {W_SLEEP, 1, 4, 135, 1, PAL_WATER, 0},
{W_OMEN, 0, 4, 150, 0, PAL_POSITIVE, 0}, {W_WEATHER, 1, 4, 170, 2, PAL_TEXT, 0},
{W_SLEEP, 0, 4, 170, 1, PAL_WATER, 0}, {W_OMEN, 1, 4, 155, 0, PAL_POSITIVE, 0},
{W_TASK_STRIP, 1, 0, 172, 1, PAL_ACCENT, 0}, {W_TASK_STRIP, 1, 0, 200, 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));
@@ -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_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_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 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", APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] snapshot ok: st=%u used=%u rem=%u",
s.status, s.used, s.cur_rem); s.status, s.used, s.cur_rem);

View File

@@ -92,6 +92,27 @@ function fetchWeather(cb) {
}, { timeout: 8000, maximumAge: 600000 }); }, { 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) { function wxLookup(lat, lon, cb) {
// rough US bbox (CONUS + AK + HI + PR): NWS has data, else Open-Meteo // rough US bbox (CONUS + AK + HI + PR): NWS has data, else Open-Meteo
var inUS = (lat >= 17 && lat <= 72 && lon >= -179 && lon <= -66); var inUS = (lat >= 17 && lat <= 72 && lon >= -179 && lon <= -66);
@@ -274,7 +295,8 @@ function sync() {
Pebble.sendAppMessage({ serverDown: 1 }, function(){}, function(){}); Pebble.sendAppMessage({ serverDown: 1 }, function(){}, function(){});
} }
); );
sendWeather(); // fire-and-forget; separate message sendWeather();
sendPhoneBatt();
}; };
if (cachedV === q.dict) { if (cachedV === q.dict) {