watchface: first rendering baseline (canvas renderer, widget table, layout persist)
This commit is contained in:
16
nine-hazes-face/.gitignore
vendored
Normal file
16
nine-hazes-face/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Build output
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Waf artifacts
|
||||||
|
.sconsign.dblite
|
||||||
|
.lock-wafbuild
|
||||||
|
|
||||||
|
# Editor / OS noise
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Pebble tool local state
|
||||||
|
pebble.log
|
||||||
10
nine-hazes-face/.lock-waf_linux_build
Normal file
10
nine-hazes-face/.lock-waf_linux_build
Normal file
File diff suppressed because one or more lines are too long
36
nine-hazes-face/README.md
Normal file
36
nine-hazes-face/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# nine-hazes-face
|
||||||
|
|
||||||
|
A Pebble watchapp/watchface written in C using the Pebble SDK.
|
||||||
|
|
||||||
|
## Building & running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pebble build # build for all targetPlatforms
|
||||||
|
pebble install --emulator emery # install on the emery emulator
|
||||||
|
pebble install --phone <ip> # install to a paired phone
|
||||||
|
```
|
||||||
|
|
||||||
|
## Target platforms
|
||||||
|
|
||||||
|
`targetPlatforms` in `package.json` controls which watches you build for. The
|
||||||
|
modern Pebble hardware is **emery** (Pebble Time 2), **gabbro** (Pebble Round
|
||||||
|
2), and **flint** (Pebble 2 Duo); the original Pebble platforms (aplite,
|
||||||
|
basalt, chalk, diorite) are included by default for backwards compatibility.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/c/ C source for the watchapp
|
||||||
|
src/pkjs/ PebbleKit JS (phone-side) source, if any
|
||||||
|
worker_src/c/ Background worker source, if any
|
||||||
|
resources/ Images, fonts, and other bundled resources
|
||||||
|
package.json Project metadata (UUID, platforms, resources, message keys)
|
||||||
|
wscript Build rules — usually no need to edit
|
||||||
|
```
|
||||||
|
|
||||||
|
By default this project is configured as a watchapp. To make it a watchface,
|
||||||
|
set `pebble.watchapp.watchface` to `true` in `package.json`.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full SDK docs, tutorials, and API reference: <https://developer.repebble.com>
|
||||||
47
nine-hazes-face/dev
Executable file
47
nine-hazes-face/dev
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# nine-hazes-face dev helpers
|
||||||
|
# Usage:
|
||||||
|
# ./dev.sh build + install on emulator
|
||||||
|
# ./dev.sh run same, plus stream watch logs (Ctrl-C detaches, app keeps running)
|
||||||
|
# ./dev.sh log attach logs only (emulator must be open)
|
||||||
|
# ./dev.sh kill stop the emulator and free the websocket
|
||||||
|
# ./dev.sh clean full rebuild (removes ./build)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
EMU="emery"
|
||||||
|
|
||||||
|
build_install() {
|
||||||
|
echo "==> Building ($EMU)..."
|
||||||
|
pebble build
|
||||||
|
echo "==> Installing to emulator..."
|
||||||
|
pebble install --emulator "$EMU"
|
||||||
|
echo "==> Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
"")
|
||||||
|
build_install
|
||||||
|
;;
|
||||||
|
run)
|
||||||
|
build_install
|
||||||
|
echo "==> Streaming logs (Ctrl-C to detach; app keeps running)..."
|
||||||
|
pebble logs --emulator "$EMU"
|
||||||
|
;;
|
||||||
|
log)
|
||||||
|
pebble logs --emulator "$EMU"
|
||||||
|
;;
|
||||||
|
kill)
|
||||||
|
pebble kill-pebble
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
rm -rf build
|
||||||
|
echo "==> ./build removed; next run is a full rebuild."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $1" >&2
|
||||||
|
sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
23
nine-hazes-face/package.json
Normal file
23
nine-hazes-face/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "nine-hazes-face",
|
||||||
|
"author": "Artimidorus",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"keywords": ["pebble-app"],
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {},
|
||||||
|
"pebble": {
|
||||||
|
"displayName": "nine-hazes-face",
|
||||||
|
"uuid": "cc8b7604-d956-4c6c-a0be-f33bec671181",
|
||||||
|
"sdkVersion": "3",
|
||||||
|
"enableMultiJS": true,
|
||||||
|
"targetPlatforms": ["emery"],
|
||||||
|
"watchapp": {
|
||||||
|
"watchface": true
|
||||||
|
},
|
||||||
|
"messageKeys": ["taskName", "taskEnd", "queueFill"],
|
||||||
|
"resources": {
|
||||||
|
"media": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
nine-hazes-face/src/c/face_packet.c
Normal file
27
nine-hazes-face/src/c/face_packet.c
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#include <pebble.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "face_packet.h"
|
||||||
|
|
||||||
|
static FacePacket s_packet;
|
||||||
|
|
||||||
|
void face_packet_init(void) {
|
||||||
|
if (persist_exists(PACKET_KEY) &&
|
||||||
|
persist_read_data(PACKET_KEY, &s_packet, sizeof(s_packet)) ==
|
||||||
|
(int)sizeof(s_packet) &&
|
||||||
|
s_packet.version == PACKET_VERSION) {
|
||||||
|
return; // cached packet restored — countdown resumes from end_epoch
|
||||||
|
}
|
||||||
|
memset(&s_packet, 0, sizeof(s_packet));
|
||||||
|
s_packet.version = PACKET_VERSION;
|
||||||
|
persist_write_data(PACKET_KEY, &s_packet, sizeof(s_packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
void face_packet_store(const FacePacket *p) {
|
||||||
|
if (!p || p->version != PACKET_VERSION) return;
|
||||||
|
s_packet = *p;
|
||||||
|
persist_write_data(PACKET_KEY, &s_packet, sizeof(s_packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
const FacePacket *face_packet_get(void) {
|
||||||
|
return &s_packet;
|
||||||
|
}
|
||||||
20
nine-hazes-face/src/c/face_packet.h
Normal file
20
nine-hazes-face/src/c/face_packet.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
// The face's ONLY view of game truth (DESIGN.md §4.1). Small, cacheable,
|
||||||
|
// stale-detectable. End timestamp is authoritative: countdown is local math.
|
||||||
|
#define PACKET_VERSION 1
|
||||||
|
#define PACKET_KEY 101
|
||||||
|
#define TASK_NAME_MAX 24
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t version; // PACKET_VERSION
|
||||||
|
char task_name[TASK_NAME_MAX]; // "" = no hero / not linked
|
||||||
|
uint32_t end_epoch; // UTC, chunk end of current task (0 = none)
|
||||||
|
uint8_t queue_fill; // chunks used, 0..36
|
||||||
|
uint32_t fetched_epoch; // when JS delivered this (staleness signal)
|
||||||
|
} FacePacket;
|
||||||
|
|
||||||
|
void face_packet_init(void); // load-or-default from persist
|
||||||
|
void face_packet_store(const FacePacket *p); // JS delivered a fresh one
|
||||||
|
const FacePacket *face_packet_get(void);
|
||||||
275
nine-hazes-face/src/c/nine-hazes-face.c
Normal file
275
nine-hazes-face/src/c/nine-hazes-face.c
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
// ===========================================================================
|
||||||
|
// Nine Hazes — watchface (v0.1 clean rewrite, Muninn-lifecycle pattern)
|
||||||
|
// Replace the WHOLE file with this. Do not splice.
|
||||||
|
// ===========================================================================
|
||||||
|
#include <pebble.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "widgets.h"
|
||||||
|
#include "face_packet.h"
|
||||||
|
|
||||||
|
static Window *s_window;
|
||||||
|
static Layer *s_canvas;
|
||||||
|
static LayoutState s_layout;
|
||||||
|
static char s_rendered[WIDGET_SLOTS][64];
|
||||||
|
static char s_clock[8], s_date[16], s_strip[64];
|
||||||
|
static int s_bat_pct = -1;
|
||||||
|
|
||||||
|
// ---- battery estimator (unchanged design) ---------------------------------
|
||||||
|
#define BAT_SAMPLES_MAX 16
|
||||||
|
#define BAT_KEY 102
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t version;
|
||||||
|
uint8_t count;
|
||||||
|
uint32_t ts[BAT_SAMPLES_MAX];
|
||||||
|
uint8_t pct[BAT_SAMPLES_MAX];
|
||||||
|
} BatLog;
|
||||||
|
static BatLog s_batlog;
|
||||||
|
|
||||||
|
static void bat_sample(uint8_t pct) {
|
||||||
|
if (pct > 100) return;
|
||||||
|
if (s_batlog.count > 0 && s_batlog.pct[s_batlog.count - 1] == pct) return;
|
||||||
|
if (s_batlog.count == BAT_SAMPLES_MAX) {
|
||||||
|
memmove(&s_batlog.ts[0], &s_batlog.ts[1], (BAT_SAMPLES_MAX - 1) * sizeof(uint32_t));
|
||||||
|
memmove(&s_batlog.pct[0], &s_batlog.pct[1], (BAT_SAMPLES_MAX - 1) * sizeof(uint8_t));
|
||||||
|
s_batlog.count--;
|
||||||
|
}
|
||||||
|
s_batlog.ts[s_batlog.count] = (uint32_t)(time(NULL) / 60);
|
||||||
|
s_batlog.pct[s_batlog.count] = pct;
|
||||||
|
s_batlog.count++;
|
||||||
|
persist_write_data(BAT_KEY, &s_batlog, sizeof(s_batlog));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int bat_days_remaining(void) {
|
||||||
|
if (s_batlog.count < 3) return -1;
|
||||||
|
int n = s_batlog.count;
|
||||||
|
double sx = 0, sy = 0, sxy = 0, sxx = 0;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
double x = (double)s_batlog.ts[i] / 1440.0;
|
||||||
|
double y = (double)s_batlog.pct[i];
|
||||||
|
sx += x; sy += y; sxy += x * y; sxx += x * x;
|
||||||
|
}
|
||||||
|
double denom = n * sxx - sx * sx;
|
||||||
|
if (denom <= 0) return -1;
|
||||||
|
double slope = (n * sxy - sx * sy) / denom;
|
||||||
|
if (slope >= -0.05) return -1;
|
||||||
|
return (int)((double)s_batlog.pct[n - 1] / -slope + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- palette & fonts --------------------------------------------------------
|
||||||
|
static GColor pal_color(PalIndex i) {
|
||||||
|
switch (i) {
|
||||||
|
case PAL_BG: return GColorWhite;
|
||||||
|
case PAL_TEXT: return GColorBlack;
|
||||||
|
case PAL_ACCENT: return GColorFromHEX(0xB08D3F);
|
||||||
|
case PAL_POSITIVE: return GColorFromHEX(0x5A7A4A);
|
||||||
|
case PAL_WARNING: return GColorFromHEX(0x6E1F24);
|
||||||
|
case PAL_WATER: return GColorFromHEX(0x4A5D73);
|
||||||
|
default: return GColorBlack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GFont s_fonts[5];
|
||||||
|
static void fonts_load(void) {
|
||||||
|
s_fonts[0] = fonts_get_system_font(FONT_KEY_GOTHIC_14);
|
||||||
|
s_fonts[1] = fonts_get_system_font(FONT_KEY_GOTHIC_18);
|
||||||
|
s_fonts[2] = fonts_get_system_font(FONT_KEY_GOTHIC_24);
|
||||||
|
s_fonts[3] = fonts_get_system_font(FONT_KEY_GOTHIC_28);
|
||||||
|
s_fonts[4] = fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- values -----------------------------------------------------------------
|
||||||
|
static void update_values(void) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm *t = localtime(&now);
|
||||||
|
strftime(s_clock, sizeof(s_clock),
|
||||||
|
clock_is_24h_style() ? "%H:%M" : "%l:%M", t);
|
||||||
|
strftime(s_date, sizeof(s_date), "%a %d %b", t);
|
||||||
|
|
||||||
|
const FacePacket *p = face_packet_get();
|
||||||
|
if (p->end_epoch > 0 && p->task_name[0]) {
|
||||||
|
int32_t secs = (int32_t)(p->end_epoch - (uint32_t)now);
|
||||||
|
if (secs > 0) {
|
||||||
|
int32_t m = secs / 60 + ((secs % 60) ? 1 : 0);
|
||||||
|
snprintf(s_strip, sizeof(s_strip), "%s %ld:%02ld %u/36",
|
||||||
|
p->task_name, (long)(m / 60), (long)(m % 60), p->queue_fill);
|
||||||
|
} else {
|
||||||
|
snprintf(s_strip, sizeof(s_strip), "%s - return for reward", p->task_name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snprintf(s_strip, sizeof(s_strip), "No hero yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- renderer -----------------------------------------------------------------
|
||||||
|
static void canvas_update(Layer *layer, GContext *ctx) {
|
||||||
|
static int n = 0;
|
||||||
|
if (n < 5) { APP_LOG(APP_LOG_LEVEL_DEBUG, "[RENDER] pass %d", n); n++; }
|
||||||
|
|
||||||
|
GRect b = layer_get_bounds(layer);
|
||||||
|
graphics_context_set_fill_color(ctx, pal_color(PAL_BG));
|
||||||
|
graphics_fill_rect(ctx, b, 0, GCornerNone);
|
||||||
|
|
||||||
|
update_values(); // ALWAYS fresh values before drawing
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < s_layout.count; i++) {
|
||||||
|
const WidgetCfg *w = &s_layout.widgets[i];
|
||||||
|
if (!w->visible || w->type >= WIDGET_TYPE_COUNT) continue;
|
||||||
|
|
||||||
|
const char *text;
|
||||||
|
char buf[64];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
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));
|
||||||
|
GRect box = GRect(w->x, w->y, b.size.w - w->x, 40);
|
||||||
|
graphics_draw_text(ctx, text, f, box, GTextOverflowModeTrailingEllipsis,
|
||||||
|
GTextAlignmentLeft, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void force_full_redraw(void) {
|
||||||
|
for (int i = 0; i < WIDGET_SLOTS; i++) s_rendered[i][0] = '\0';
|
||||||
|
if (s_canvas) layer_mark_dirty(s_canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- layout (persist-or-default) ---------------------------------------------
|
||||||
|
static void layout_default(void) {
|
||||||
|
memset(&s_layout, 0, sizeof(s_layout));
|
||||||
|
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_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},
|
||||||
|
};
|
||||||
|
memcpy(s_layout.widgets, defs, sizeof(defs));
|
||||||
|
persist_write_data(LAYOUT_KEY, &s_layout, sizeof(s_layout));
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[LAYOUT] defaults written");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void layout_init(void) {
|
||||||
|
if (persist_exists(LAYOUT_KEY)) {
|
||||||
|
int read = persist_read_data(LAYOUT_KEY, &s_layout, sizeof(s_layout));
|
||||||
|
bool ok = (read == (int)sizeof(s_layout)) &&
|
||||||
|
(s_layout.version == LAYOUT_VERSION) &&
|
||||||
|
(s_layout.count > 0 && s_layout.count <= WIDGET_SLOTS);
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[LAYOUT] read=%d ok=%d count=%u", read, ok, s_layout.count);
|
||||||
|
if (ok) return;
|
||||||
|
}
|
||||||
|
layout_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- services ------------------------------------------------------------------
|
||||||
|
static void tick_handler(struct tm *t, TimeUnits u) {
|
||||||
|
static int n = 0;
|
||||||
|
if (n < 3) { APP_LOG(APP_LOG_LEVEL_DEBUG, "[TICK] %02d:%02d", t->tm_hour, t->tm_min); n++; }
|
||||||
|
if (s_canvas) layer_mark_dirty(s_canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void battery_handler(BatteryChargeState state) {
|
||||||
|
s_bat_pct = state.charge_percent;
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[BAT] %d%%", s_bat_pct);
|
||||||
|
bat_sample((uint8_t)state.charge_percent);
|
||||||
|
if (s_canvas) layer_mark_dirty(s_canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- AppMessage -------------------------------------------------------------
|
||||||
|
static void inbox_received(DictionaryIterator *iter, void *ctx) {
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[MSG] inbox received");
|
||||||
|
FacePacket p = *face_packet_get();
|
||||||
|
Tuple *t = dict_find(iter, MESSAGE_KEY_taskName);
|
||||||
|
if (t) { strncpy(p.task_name, t->value->cstring, TASK_NAME_MAX - 1);
|
||||||
|
p.task_name[TASK_NAME_MAX - 1] = '\0'; }
|
||||||
|
if ((t = dict_find(iter, MESSAGE_KEY_taskEnd))) p.end_epoch = t->value->uint32;
|
||||||
|
if ((t = dict_find(iter, MESSAGE_KEY_queueFill))) p.queue_fill = t->value->uint8;
|
||||||
|
p.fetched_epoch = (uint32_t)time(NULL);
|
||||||
|
face_packet_store(&p);
|
||||||
|
force_full_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- window lifecycle ---------------------------------------------------------
|
||||||
|
static void window_load(Window *w) {
|
||||||
|
Layer *root = window_get_root_layer(w);
|
||||||
|
GRect b = layer_get_bounds(root);
|
||||||
|
s_canvas = layer_create(b);
|
||||||
|
layer_set_update_proc(s_canvas, canvas_update);
|
||||||
|
layer_add_child(root, s_canvas);
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[LOAD] canvas up, count=%u w0=%u", s_layout.count, s_layout.widgets[0].type);
|
||||||
|
update_values();
|
||||||
|
force_full_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void window_unload(Window *w) {
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[UNLOAD]");
|
||||||
|
layer_destroy(s_canvas);
|
||||||
|
s_canvas = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- app lifecycle (Muninn pattern: all state first, then window) -------------
|
||||||
|
static void init(void) {
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[INIT] start");
|
||||||
|
fonts_load();
|
||||||
|
layout_init();
|
||||||
|
face_packet_init();
|
||||||
|
|
||||||
|
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_window = window_create();
|
||||||
|
window_set_window_handlers(s_window, (WindowHandlers){
|
||||||
|
.load = window_load, .unload = window_unload });
|
||||||
|
window_stack_push(s_window, true);
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[INIT] window pushed");
|
||||||
|
|
||||||
|
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(64, 64);
|
||||||
|
APP_LOG(APP_LOG_LEVEL_DEBUG, "[INIT] done");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void deinit(void) {
|
||||||
|
tick_timer_service_unsubscribe();
|
||||||
|
battery_state_service_unsubscribe();
|
||||||
|
window_destroy(s_window);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
init();
|
||||||
|
app_event_loop();
|
||||||
|
deinit();
|
||||||
|
}
|
||||||
53
nine-hazes-face/src/c/widgets.h
Normal file
53
nine-hazes-face/src/c/widgets.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
// ---- Widget types (10, locked in DESIGN.md §5) ---------------------------
|
||||||
|
typedef enum {
|
||||||
|
W_TIME = 0,
|
||||||
|
W_DATE,
|
||||||
|
W_BATTERY, // watch battery; percent | days | both modes
|
||||||
|
W_PHONE_BATTERY,
|
||||||
|
W_STEPS,
|
||||||
|
W_DISTANCE,
|
||||||
|
W_WEATHER, // composite: current temp + hi/lo (sub-elements later)
|
||||||
|
W_OMEN, // weather advisory flavor strip
|
||||||
|
W_SLEEP,
|
||||||
|
W_TASK_STRIP, // game presence — the one game element on the face
|
||||||
|
WIDGET_TYPE_COUNT
|
||||||
|
} WidgetType;
|
||||||
|
|
||||||
|
// ---- Celtic palette (DESIGN.md §9) — indexed, never raw ---------------
|
||||||
|
typedef enum {
|
||||||
|
PAL_BG = 0, // peat black
|
||||||
|
PAL_TEXT, // parchment white
|
||||||
|
PAL_ACCENT, // aged brass
|
||||||
|
PAL_POSITIVE, // moss green
|
||||||
|
PAL_WARNING, // oxblood
|
||||||
|
PAL_WATER, // river slate
|
||||||
|
PAL_COUNT
|
||||||
|
} PalIndex;
|
||||||
|
|
||||||
|
// Battery widget display mode
|
||||||
|
typedef enum { BAT_PERCENT = 0, BAT_DAYS, BAT_BOTH } BatMode;
|
||||||
|
|
||||||
|
// ---- Packed, persisted widget config -------------------------------------
|
||||||
|
// Layout is first-class: OWN persist key, OWN version byte (DESIGN.md §4).
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t type; // WidgetType
|
||||||
|
uint8_t visible; // bool
|
||||||
|
int16_t x; // top-left, screen coords (200x228)
|
||||||
|
int16_t y;
|
||||||
|
uint8_t font_idx; // index into font table
|
||||||
|
uint8_t color_idx; // PalIndex
|
||||||
|
uint8_t aux; // per-type: BatMode for battery, sub-flags later
|
||||||
|
} WidgetCfg;
|
||||||
|
|
||||||
|
#define LAYOUT_VERSION 1
|
||||||
|
#define LAYOUT_KEY 100
|
||||||
|
#define WIDGET_SLOTS 12 // 10 types + 2 spare, matching "12" scaffold note
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t version;
|
||||||
|
uint8_t count;
|
||||||
|
WidgetCfg widgets[WIDGET_SLOTS];
|
||||||
|
} LayoutState;
|
||||||
8
nine-hazes-face/src/pkjs/index.js
Normal file
8
nine-hazes-face/src/pkjs/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Nine Hazes watchface pkjs. Phase 1: hello only.
|
||||||
|
// Later (per DESIGN.md §4.1): on 'ready', fetch the face packet from the
|
||||||
|
// configured server (default instance, self-hostable override) respecting the
|
||||||
|
// staleness timestamp, then Pebble.sendAppMessage({taskName, taskEnd,
|
||||||
|
// queueFill}). All fetches wrapped: server down -> render cached, silently.
|
||||||
|
Pebble.addEventListener('ready', () => {
|
||||||
|
console.log('Nine Hazes pkjs ready');
|
||||||
|
});
|
||||||
54
nine-hazes-face/wscript
Normal file
54
nine-hazes-face/wscript
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#
|
||||||
|
# This file is the default set of rules to compile a Pebble application.
|
||||||
|
#
|
||||||
|
# Feel free to customize this to your needs.
|
||||||
|
#
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
top = '.'
|
||||||
|
out = 'build'
|
||||||
|
|
||||||
|
|
||||||
|
def options(ctx):
|
||||||
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
|
|
||||||
|
def configure(ctx):
|
||||||
|
"""
|
||||||
|
This method is used to configure your build. ctx.load(`pebble_sdk`) automatically configures
|
||||||
|
a build for each valid platform in `targetPlatforms`. Platform-specific configuration: add your
|
||||||
|
change after calling ctx.load('pebble_sdk') and make sure to set the correct environment first.
|
||||||
|
Universal configuration: add your change prior to calling ctx.load('pebble_sdk').
|
||||||
|
"""
|
||||||
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
|
|
||||||
|
def build(ctx):
|
||||||
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
|
build_worker = os.path.exists('worker_src')
|
||||||
|
binaries = []
|
||||||
|
|
||||||
|
cached_env = ctx.env
|
||||||
|
for platform in ctx.env.TARGET_PLATFORMS:
|
||||||
|
ctx.env = ctx.all_envs[platform]
|
||||||
|
ctx.set_group(ctx.env.PLATFORM_NAME)
|
||||||
|
app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR)
|
||||||
|
ctx.pbl_build(source=ctx.path.ant_glob('src/c/**/*.c'), target=app_elf, bin_type='app')
|
||||||
|
|
||||||
|
if build_worker:
|
||||||
|
worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR)
|
||||||
|
binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf})
|
||||||
|
ctx.pbl_build(source=ctx.path.ant_glob('worker_src/c/**/*.c'),
|
||||||
|
target=worker_elf,
|
||||||
|
bin_type='worker')
|
||||||
|
else:
|
||||||
|
binaries.append({'platform': platform, 'app_elf': app_elf})
|
||||||
|
ctx.env = cached_env
|
||||||
|
|
||||||
|
ctx.set_group('bundle')
|
||||||
|
ctx.pbl_bundle(binaries=binaries,
|
||||||
|
js=ctx.path.ant_glob(['src/pkjs/**/*.js',
|
||||||
|
'src/pkjs/**/*.json',
|
||||||
|
'src/common/**/*.js']),
|
||||||
|
js_entry_file='src/pkjs/index.js')
|
||||||
Reference in New Issue
Block a user