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",
"taskCount", "curType", "curRem", "status", "abortCode",
"pendItems", "pendXp", "dictV", "serverDown", "requestSync",
"weatherTemp", "weatherHi", "weatherLo", "weatherTs", "weatherCond"],
"weatherTemp", "weatherHi", "weatherLo", "weatherTs",
"weatherCond", "phoneBatt"],
"resources": {
"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 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);
@@ -268,14 +305,14 @@ static void layout_default(void) {
{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_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_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_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);

View File

@@ -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) {