Introduces watch/, a real Pebble watchapp (built via pebble build) that
build / build (push) Successful in 30s

shows the daily tarot/astrology reading on-device: persistence, reading
computation, and UI screens, driven by the existing engine/interpreter
code linked in unchanged where possible.

Required engine-side changes to make that linking work:
- A compact low-precision ephemeris (lowprec_ephemeris.c) replacing the
  vendored ~127KB Astronomy Engine, which doesn't fit the watch's ~64KB
  app budget.
- Hand-rolled sqrt/atan2/sin/cos replacements for that ephemeris and
  astro.c's Ascendant calculation - Pebble's statically-linked libm
  hard-faults on real hardware under this app's -fPIE link for all four.
- narrative.c/guidance.c refactored from FILE*/fprintf onto snprintf-
  based buffers, since Pebble's SDK blocks fprintf at compile time.
This commit is contained in:
ml
2026-07-14 17:10:22 +02:00
parent d9b6e2889b
commit 1a654da3e9
44 changed files with 1920 additions and 182 deletions
+59
View File
@@ -0,0 +1,59 @@
# Deck in a Dash - Pebble watch app
The actual watchapp: shows the same daily tarot + astrology reading as
`dist/run-interpreter.sh`, computed entirely on-device (no companion
app, no server - birth data, language, and notification settings are
entered via a Pebble config page and persisted on-watch).
## Layout
```
watch/
package.json Pebble app manifest (uuid, targetPlatforms, messageKeys, resources)
wscript waf build rules - see its own top-of-file comment for
why this app's C sources are NOT a plain src/c/**/*.c
glob (most of the actual logic lives in ../engine/src/,
shared byte-for-byte with the desktop deck-engine/
interpreter-cli binaries)
src/c/ Watch-only C: UI, persisted config, on-device reading
orchestration, daily notification wakeup
src/pkjs/ pkjs config webview (birth data/language/notification
settings) - a self-contained `data:` URI form, no
server
resources/ Bitmap resources (card art, app icon) - resources/img/
is generated (gitignored), see below
scripts/ Build-time codegen: gen_i18n_tables.py (translation
tables from ../engine/i18n/*.lang) and
gen_card_images.py (downsized card art from
../res/img/*.jpeg) - both run automatically by
wscript on every `pebble build` (see its own comment
for why that stays cheap on repeat builds)
```
## Building
Requires the Pebble SDK/tool (`pebble` CLI) and Pillow (`pip install
Pillow`, used only by gen_card_images.py) - neither is part of this
repo's own `make`/`make test` toolchain, which only builds the desktop
`deck-engine`/`interpreter-cli` binaries. From this directory:
```bash
pebble build # -> build/watch.pbw
```
The root Makefile's `make watchapp` target wraps this and copies the
resulting `.pbw` into `dist/`, versioned the same way as `make package` -
see the root CLAUDE.md's "Packaging" section.
## Platform support
`targetPlatforms` deliberately excludes **aplite** (original Pebble /
Pebble Steel). A Pebble app's entire code+data+bss footprint shares one
64KB region on basalt-class platforms, but only 24KB on aplite - and
even this project's minimal skeleton (blank window + one
`reading_generate()` call, no UI/config/persistence/notification code
yet) already uses 19KB of it, leaving only ~5.5KB of headroom. That's
not enough room for the multi-screen UI, on-watch config persistence,
and wakeup-based daily notification this app still needs to add -
basalt and newer (chalk/diorite/emery/flint/gabbro) all have comfortable
46-112KB of free heap for the same skeleton.
+65
View File
@@ -0,0 +1,65 @@
{
"name": "deck_in_a_dash_watch",
"author": "Matthias Ladkau",
"version": "1.0.0",
"keywords": ["pebble-app"],
"private": true,
"dependencies": {},
"pebble": {
"displayName": "Deck in a Dash",
"uuid": "7f47ed93-f44e-4c1a-9998-7edcfa61218e",
"sdkVersion": "3",
"enableMultiJS": true,
"targetPlatforms": [
"basalt",
"chalk",
"diorite",
"emery",
"flint",
"gabbro"
],
"watchapp": {
"watchface": false
},
"messageKeys": [
"BIRTH_YEAR",
"BIRTH_MONTH",
"BIRTH_DAY",
"BIRTH_HOUR",
"BIRTH_MINUTE",
"BIRTH_UTC_OFFSET_MINUTES",
"BIRTH_LAT_MICRODEG",
"BIRTH_LON_MICRODEG",
"LANG",
"NOTIFY_ENABLED",
"NOTIFY_HOUR",
"NOTIFY_MINUTE"
],
"resources": {
"media": [
{ "type": "bitmap", "name": "IMG_CARD_FOOL", "file": "img/fool.png" },
{ "type": "bitmap", "name": "IMG_CARD_MAGICIAN", "file": "img/magician.png" },
{ "type": "bitmap", "name": "IMG_CARD_HIGH_PRIESTESS", "file": "img/high_priestess.png" },
{ "type": "bitmap", "name": "IMG_CARD_EMPRESS", "file": "img/empress.png" },
{ "type": "bitmap", "name": "IMG_CARD_EMPEROR", "file": "img/emperor.png" },
{ "type": "bitmap", "name": "IMG_CARD_HIEROPHANT", "file": "img/hierophant.png" },
{ "type": "bitmap", "name": "IMG_CARD_LOVERS", "file": "img/lovers.png" },
{ "type": "bitmap", "name": "IMG_CARD_CHARIOT", "file": "img/chariot.png" },
{ "type": "bitmap", "name": "IMG_CARD_STRENGTH", "file": "img/strength.png" },
{ "type": "bitmap", "name": "IMG_CARD_HERMIT", "file": "img/hermit.png" },
{ "type": "bitmap", "name": "IMG_CARD_WHEEL_OF_FORTUNE", "file": "img/wheel_of_fortune.png" },
{ "type": "bitmap", "name": "IMG_CARD_JUSTICE", "file": "img/justice.png" },
{ "type": "bitmap", "name": "IMG_CARD_HANGED_MAN", "file": "img/hanged_man.png" },
{ "type": "bitmap", "name": "IMG_CARD_DEATH", "file": "img/death.png" },
{ "type": "bitmap", "name": "IMG_CARD_TEMPERANCE", "file": "img/temperance.png" },
{ "type": "bitmap", "name": "IMG_CARD_DEVIL", "file": "img/devil.png" },
{ "type": "bitmap", "name": "IMG_CARD_TOWER", "file": "img/tower.png" },
{ "type": "bitmap", "name": "IMG_CARD_STAR", "file": "img/star.png" },
{ "type": "bitmap", "name": "IMG_CARD_MOON", "file": "img/moon.png" },
{ "type": "bitmap", "name": "IMG_CARD_SUN", "file": "img/sun.png" },
{ "type": "bitmap", "name": "IMG_CARD_JUDGEMENT", "file": "img/judgement.png" },
{ "type": "bitmap", "name": "IMG_CARD_WORLD", "file": "img/world.png" }
]
}
}
}
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Downsizes res/img/*.jpeg into watch/resources/img/*.png for Pebble.
The 22 Major Arcana JPEGs in res/img/ are ~1MB/~1086x1810px each -
suitable for the desktop HTML reading (an <img> tag pointed at the file
as-is) but far too large for a Pebble app's own resource budget (256KB
total on basalt-class platforms, shared with every other resource this
app ships). This script resizes each to CARD_WIDTH px wide (preserving
aspect ratio) and writes a PNG per card, named after tarot_data.c's own
k_card_slug table so watch/package.json's "resources.media" entries
(IMG_CARD_<SLUG UPPERCASE>, file img/<slug>.png) line up without a
separate mapping table anywhere.
CARD_WIDTH=72 (-> 72x120) was picked empirically: a real `pebble build`
of two sample cards at this size measured ~5KB/card on color platforms
(basalt/chalk/emery/gabbro) and ~2.9KB/card on the black-and-white
diorite/flint - so all 22 cards together cost roughly 111KB/64KB
respectively, comfortably inside the 256KB resource budget with room
left for other future resources (fonts, icons), while still rendering
at close to a full basalt-class screen's height (120px of 168px).
Generated output, like i18n_tables.auto.c - not committed (see
.gitignore), regenerate by running this script directly or via the root
Makefile's `make watchapp` (task/target that also runs `pebble build`).
Requires Pillow (`pip install Pillow`), not part of this repo's own
desktop build/test toolchain.
"""
import os
import sys
from PIL import Image
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SRC_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "res", "img"))
OUT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "resources", "img"))
CARD_WIDTH = 72
# (source file in res/img/, slug matching engine/src/tarot_data.c's
# k_card_slug table) - in TarotCard enum order (CARD_FOOL = 0 ... CARD_WORLD).
CARDS = [
("RWS_Tarot_00_Fool.jpeg", "fool"),
("RWS_Tarot_01_Magician.jpeg", "magician"),
("RWS_Tarot_02_High_Priestess.jpeg", "high_priestess"),
("RWS_Tarot_03_Empress.jpeg", "empress"),
("RWS_Tarot_04_Emperor.jpeg", "emperor"),
("RWS_Tarot_05_Hierophant.jpeg", "hierophant"),
("RWS_Tarot_06_Lovers.jpeg", "lovers"),
("RWS_Tarot_07_Chariot.jpeg", "chariot"),
("RWS_Tarot_08_Strength.jpeg", "strength"),
("RWS_Tarot_09_Hermit.jpeg", "hermit"),
("RWS_Tarot_10_Wheel_of_Fortune.jpeg", "wheel_of_fortune"),
("RWS_Tarot_11_Justice.jpeg", "justice"),
("RWS_Tarot_12_Hanged_Man.jpeg", "hanged_man"),
("RWS_Tarot_13_Death.jpeg", "death"),
("RWS_Tarot_14_Temperance.jpeg", "temperance"),
("RWS_Tarot_15_Devil.jpeg", "devil"),
("RWS_Tarot_16_Tower.jpeg", "tower"),
("RWS_Tarot_17_Star.jpeg", "star"),
("RWS_Tarot_18_Moon.jpeg", "moon"),
("RWS_Tarot_19_Sun.jpeg", "sun"),
("RWS_Tarot_20_Judgement.jpeg", "judgement"),
("RWS_Tarot_21_World.jpeg", "world"),
]
def main():
os.makedirs(OUT_DIR, exist_ok=True)
written = 0
for src_name, slug in CARDS:
src_path = os.path.join(SRC_DIR, src_name)
out_path = os.path.join(OUT_DIR, f"{slug}.png")
# wscript runs this on every `pebble build` (like gen_i18n_tables.py)
# so a full 22-image resize must be cheap on the common no-op case -
# skip any card whose output is already newer than its source JPEG.
if os.path.exists(out_path) and os.path.getmtime(out_path) >= os.path.getmtime(src_path):
continue
with Image.open(src_path) as img:
ratio = CARD_WIDTH / img.width
height = round(img.height * ratio)
resized = img.convert("RGB").resize((CARD_WIDTH, height), Image.LANCZOS)
resized.save(out_path, "PNG", optimize=True)
written += 1
print(f"wrote {written}/{len(CARDS)} card images to {OUT_DIR}", file=sys.stderr)
if __name__ == "__main__":
main()
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Generates watch/src/c/i18n_tables.auto.c from engine/i18n/*.lang.
Compiles each non-English .lang file into a `static const char *const`
keys[]/values[] pair for i18n_load_table() (see engine/src/i18n.h's own
doc comment on why the watch needs a zero-copy, compiled-in table
instead of i18n_load()'s file-based parsing). English is deliberately
skipped: the fallback text already baked into tarot_data.c/astro.c/
narrative.c/guidance.c *is* English, so shipping a redundant English
catalog would roughly double this app's translation-data footprint
(which counts against the same ~64KB whole-app budget as everything
else - see watch/README.md) for zero behavioural benefit.
This is a generated file (like message_keys.auto.c, resource_ids.auto.c
elsewhere in a Pebble project) - not meant to be hand-edited or
committed; watch/wscript regenerates it on every build. Parsing rules
mirror engine/src/i18n.c's i18n_load() exactly: '#'-prefixed and blank
lines are skipped, the first '=' splits key/value, and both sides are
trimmed of surrounding spaces/tabs only (not quotes - values are raw
text, same as the desktop file loader).
"""
import glob
import os
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
I18N_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "engine", "i18n"))
OUT_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "src", "c", "i18n_tables.auto.c"))
def parse_lang_file(path):
entries = []
with open(path, "r", encoding="utf-8") as f:
for raw_line in f:
line = raw_line.rstrip("\r\n")
stripped = line.strip(" \t")
if not stripped or stripped.startswith("#"):
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip(" \t")
value = value.strip(" \t")
if not key:
continue
entries.append((key, value))
return entries
def c_string_literal(s):
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
return '"' + escaped + '"'
def main():
lang_files = sorted(glob.glob(os.path.join(I18N_DIR, "*.lang")))
languages = []
for path in lang_files:
code = os.path.splitext(os.path.basename(path))[0]
if code == "en":
continue # see module doc comment - English is never a compiled table
languages.append((code, parse_lang_file(path)))
lines = []
lines.append("/* Generated by watch/scripts/gen_i18n_tables.py from engine/i18n's .lang")
lines.append(" * files - do not edit by hand, and do not commit (see .gitignore). */")
lines.append("")
lines.append('#include "i18n_tables.h"')
lines.append("")
lines.append('#include "../../../engine/src/i18n.h"')
lines.append("")
lines.append("#include <stddef.h>")
lines.append("#include <string.h>")
lines.append("")
for code, entries in languages:
lines.append(f"static const char *const k_keys_{code}[] = {{")
for key, _ in entries:
lines.append(f" {c_string_literal(key)},")
lines.append("};")
lines.append(f"static const char *const k_values_{code}[] = {{")
for _, value in entries:
lines.append(f" {c_string_literal(value)},")
lines.append("};")
lines.append("")
lines.append("void watch_i18n_load(const char *lang_code) {")
for code, entries in languages:
lines.append(f' if (lang_code && strcmp(lang_code, "{code}") == 0) {{')
lines.append(f" i18n_load_table(k_keys_{code}, k_values_{code}, {len(entries)});")
lines.append(" return;")
lines.append(" }")
lines.append(" i18n_load_table(NULL, NULL, 0); /* \"en\" or unrecognized: use built-in English */")
lines.append("}")
lines.append("")
os.makedirs(os.path.dirname(OUT_PATH), exist_ok=True)
with open(OUT_PATH, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"wrote {OUT_PATH} ({', '.join(code for code, _ in languages)})", file=sys.stderr)
if __name__ == "__main__":
main()
+83
View File
@@ -0,0 +1,83 @@
#include "app_message.h"
#include <pebble.h>
/* AppMessage's Dictionary has no float tuple type, so latitude/
* longitude/utc_offset_hours (all doubles in BirthData) cross the wire
* as scaled int32s - microdegrees for lat/lon (~0.11m precision, far
* finer than this app's astrology needs), minutes for the UTC offset
* (covers every real-world offset, including the half/quarter-hour
* ones) - see config.c's own persisted representation, which uses the
* same scaling so no conversion happens twice. */
static ConfigUpdatedCallback s_callback;
static bool read_int(DictionaryIterator *iter, uint32_t key, int32_t *out) {
Tuple *t = dict_find(iter, key);
if (!t) return false;
*out = t->value->int32;
return true;
}
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
WatchConfig cfg;
config_load(&cfg); /* start from existing settings - a field the page
* doesn't send (e.g. it only changed the language)
* keeps its previous value rather than resetting. */
int32_t v;
bool got_birth = false;
if (read_int(iter, MESSAGE_KEY_BIRTH_YEAR, &v)) { cfg.birth.year = v; got_birth = true; }
if (read_int(iter, MESSAGE_KEY_BIRTH_MONTH, &v)) { cfg.birth.month = v; got_birth = true; }
if (read_int(iter, MESSAGE_KEY_BIRTH_DAY, &v)) { cfg.birth.day = v; got_birth = true; }
if (read_int(iter, MESSAGE_KEY_BIRTH_HOUR, &v)) { cfg.birth.hour = v; got_birth = true; }
if (read_int(iter, MESSAGE_KEY_BIRTH_MINUTE, &v)) { cfg.birth.minute = v; got_birth = true; }
if (read_int(iter, MESSAGE_KEY_BIRTH_UTC_OFFSET_MINUTES, &v)) {
cfg.birth.utc_offset_hours = v / 60.0;
got_birth = true;
}
if (read_int(iter, MESSAGE_KEY_BIRTH_LAT_MICRODEG, &v)) {
cfg.birth.latitude = v / 1000000.0;
got_birth = true;
}
if (read_int(iter, MESSAGE_KEY_BIRTH_LON_MICRODEG, &v)) {
cfg.birth.longitude = v / 1000000.0;
got_birth = true;
}
Tuple *lang_tuple = dict_find(iter, MESSAGE_KEY_LANG);
if (lang_tuple) {
strncpy(cfg.lang, lang_tuple->value->cstring, sizeof(cfg.lang) - 1);
cfg.lang[sizeof(cfg.lang) - 1] = '\0';
}
if (read_int(iter, MESSAGE_KEY_NOTIFY_ENABLED, &v)) cfg.notify_enabled = (v != 0);
if (read_int(iter, MESSAGE_KEY_NOTIFY_HOUR, &v)) cfg.notify_hour = v;
if (read_int(iter, MESSAGE_KEY_NOTIFY_MINUTE, &v)) cfg.notify_minute = v;
/* Birth data is the one thing that makes a config submission "complete" -
* language/notification-only re-submissions (e.g. from a later re-open
* of the settings page) must not un-configure an already-set-up watch. */
if (got_birth) cfg.configured = true;
config_save(&cfg);
if (s_callback) s_callback(&cfg);
}
static void inbox_dropped_handler(AppMessageResult reason, void *context) {
APP_LOG(APP_LOG_LEVEL_ERROR, "AppMessage dropped: reason %d", (int)reason);
}
/* app_message_*_size_maximum() return the largest buffer the platform
* *could* hand out (8200 bytes each) - not a size to actually request.
* The real payload is ~150-200 bytes; 256 bytes each leaves headroom
* without eating into this app's ~12KB heap budget (see watch/README.md). */
#define INBOX_SIZE 256
#define OUTBOX_SIZE 256
void app_message_init(ConfigUpdatedCallback on_config_updated) {
s_callback = on_config_updated;
app_message_register_inbox_received(inbox_received_handler);
app_message_register_inbox_dropped(inbox_dropped_handler);
app_message_open(INBOX_SIZE, OUTBOX_SIZE);
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef WATCH_APP_MESSAGE_H
#define WATCH_APP_MESSAGE_H
#include "config.h"
/* Invoked with the freshly-saved config whenever a settings submission
* arrives from the config page (src/pkjs/index.js, task #43) - main.c
* uses this to recompute the reading and refresh/show the menu. */
typedef void (*ConfigUpdatedCallback)(const WatchConfig *cfg);
/* Registers the AppMessage inbox handler and opens the message buffers.
* Call once at startup, before app_event_loop(). Message keys
* (BIRTH_YEAR, BIRTH_UTC_OFFSET_MINUTES, LANG, NOTIFY_ENABLED, etc.) are
* declared in watch/package.json's "messageKeys" and turn into
* MESSAGE_KEY_* constants in the auto-generated message_keys.auto.h -
* see app_message.c's own comment on why latitude/longitude/utc offset
* travel as scaled integers rather than floats. */
void app_message_init(ConfigUpdatedCallback on_config_updated);
#endif
+70
View File
@@ -0,0 +1,70 @@
#include "config.h"
#include <string.h>
/* Persist keys. BirthData's latitude/longitude/utc_offset_hours are
* doubles, but Pebble's persist API has no float storage - stored as
* scaled int32s instead (microdegrees / minutes), matching how
* AppMessage carries the same fields from the config page - see
* app_message.c. */
enum {
PKEY_CONFIGURED = 0,
PKEY_BIRTH_YEAR,
PKEY_BIRTH_MONTH,
PKEY_BIRTH_DAY,
PKEY_BIRTH_HOUR,
PKEY_BIRTH_MINUTE,
PKEY_BIRTH_UTC_OFFSET_MINUTES,
PKEY_BIRTH_LAT_MICRODEG,
PKEY_BIRTH_LON_MICRODEG,
PKEY_LANG,
PKEY_NOTIFY_ENABLED,
PKEY_NOTIFY_HOUR,
PKEY_NOTIFY_MINUTE,
};
void config_load(WatchConfig *out) {
memset(out, 0, sizeof *out);
out->configured = persist_exists(PKEY_CONFIGURED) && persist_read_bool(PKEY_CONFIGURED);
if (!out->configured) {
strcpy(out->lang, "en");
return;
}
out->birth.year = (int)persist_read_int(PKEY_BIRTH_YEAR);
out->birth.month = (int)persist_read_int(PKEY_BIRTH_MONTH);
out->birth.day = (int)persist_read_int(PKEY_BIRTH_DAY);
out->birth.hour = (int)persist_read_int(PKEY_BIRTH_HOUR);
out->birth.minute = (int)persist_read_int(PKEY_BIRTH_MINUTE);
out->birth.utc_offset_hours = (double)(int32_t)persist_read_int(PKEY_BIRTH_UTC_OFFSET_MINUTES) / 60.0;
out->birth.latitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LAT_MICRODEG) / 1000000.0;
out->birth.longitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LON_MICRODEG) / 1000000.0;
if (persist_read_string(PKEY_LANG, out->lang, sizeof out->lang) <= 0) {
strcpy(out->lang, "en");
}
out->notify_enabled = persist_read_bool(PKEY_NOTIFY_ENABLED);
out->notify_hour = (int)persist_read_int(PKEY_NOTIFY_HOUR);
out->notify_minute = (int)persist_read_int(PKEY_NOTIFY_MINUTE);
}
void config_save(const WatchConfig *cfg) {
persist_write_bool(PKEY_CONFIGURED, cfg->configured);
persist_write_int(PKEY_BIRTH_YEAR, cfg->birth.year);
persist_write_int(PKEY_BIRTH_MONTH, cfg->birth.month);
persist_write_int(PKEY_BIRTH_DAY, cfg->birth.day);
persist_write_int(PKEY_BIRTH_HOUR, cfg->birth.hour);
persist_write_int(PKEY_BIRTH_MINUTE, cfg->birth.minute);
persist_write_int(PKEY_BIRTH_UTC_OFFSET_MINUTES, (int32_t)(cfg->birth.utc_offset_hours * 60.0));
persist_write_int(PKEY_BIRTH_LAT_MICRODEG, (int32_t)(cfg->birth.latitude * 1000000.0));
persist_write_int(PKEY_BIRTH_LON_MICRODEG, (int32_t)(cfg->birth.longitude * 1000000.0));
persist_write_string(PKEY_LANG, cfg->lang);
persist_write_bool(PKEY_NOTIFY_ENABLED, cfg->notify_enabled);
persist_write_int(PKEY_NOTIFY_HOUR, cfg->notify_hour);
persist_write_int(PKEY_NOTIFY_MINUTE, cfg->notify_minute);
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef WATCH_CONFIG_H
#define WATCH_CONFIG_H
#include <pebble.h>
#include "../../../engine/src/astro.h"
/* Everything the watch needs that isn't computed fresh each day: birth
* data (the same fields as dist/user.properties), display language, and
* daily notification settings. Persisted on-watch via Pebble's own
* key-value persist API (see config.c) - no companion app, no server,
* matching the rest of this project's architecture. */
typedef struct {
bool configured; /* false until the config page has been submitted once */
BirthData birth;
char lang[8]; /* e.g. "en", "de" - matches engine/i18n/<lang>.lang's own code */
bool notify_enabled;
int notify_hour; /* 0-23, the watch's own local wall-clock time (i.e.
* localtime(), not birth.utc_offset_hours - the
* user's current timezone, synced from their phone,
* not necessarily their birth location's). */
int notify_minute; /* 0-59 */
} WatchConfig;
/* Fills `out` with the persisted config, or a safe all-zero/`configured
* = false` default if nothing has been saved yet (first run). */
void config_load(WatchConfig *out);
void config_save(const WatchConfig *cfg);
#endif
+16
View File
@@ -0,0 +1,16 @@
#ifndef WATCH_I18N_TABLES_H
#define WATCH_I18N_TABLES_H
/* Points i18n_get() (engine/src/i18n.h) at the compiled-in translation
* table for `lang_code`, generated at build time from engine/i18n's
* .lang files by watch/scripts/gen_i18n_tables.py into
* i18n_tables.auto.c - see that script's own doc comment for the format
* and why English is deliberately not one of the compiled tables.
* Passing "en", NULL, or any code with no matching .lang file resets to
* no table at all, so every i18n_get() call falls through to its
* built-in English fallback text - the correct behavior when a user
* switches the watch's language setting back to English at runtime,
* not just on first load. */
void watch_i18n_load(const char *lang_code);
#endif
+50
View File
@@ -0,0 +1,50 @@
#include <pebble.h>
#include "../../../engine/src/i18n.h"
#include "app_message.h"
#include "config.h"
#include "i18n_tables.h"
#include "reading_state.h"
#include "ui_menu_window.h"
#include "ui_text_window.h"
static WatchConfig s_config;
static void show_setup_required(void) {
text_window_push(i18n_get("ui.setup_required_title", "Setup Required"),
i18n_get("ui.setup_required_body",
"Open this app's settings from the Pebble mobile app to add your "
"birth details, then reopen Deck in a Dash."));
}
static void on_config_updated(const WatchConfig *cfg) {
bool was_configured = s_config.configured;
s_config = *cfg;
watch_i18n_load(s_config.lang);
if (!s_config.configured) return;
reading_state_recompute(&s_config.birth);
if (was_configured) {
menu_window_reload();
} else {
menu_window_push();
}
}
int main(void) {
config_load(&s_config);
watch_i18n_load(s_config.lang);
app_message_init(on_config_updated);
if (s_config.configured) {
reading_state_recompute(&s_config.birth);
menu_window_push();
} else {
show_setup_required();
}
app_event_loop();
menu_window_deinit();
}
+27
View File
@@ -0,0 +1,27 @@
#include "reading_state.h"
#include <stdio.h>
static DailyReading s_reading;
static DailyInterpretation s_interp;
void reading_state_recompute(const BirthData *birth) {
time_t now = time(NULL);
/* Plain gmtime() (not the POSIX-only gmtime_r()), used immediately -
* same justification as astro.c's own time_from_unix(). */
struct tm *utc = gmtime(&now);
char seed[16];
snprintf(seed, sizeof seed, "%04d-%02d-%02d", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
reading_generate(seed, birth, now, &s_reading);
interpret_daily_reading(&s_reading, &s_interp);
}
const DailyReading *reading_state_get_reading(void) {
return &s_reading;
}
const DailyInterpretation *reading_state_get_interpretation(void) {
return &s_interp;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef WATCH_READING_STATE_H
#define WATCH_READING_STATE_H
#include "../../../engine/src/reading.h"
#include "../../../interpreter/src/significance.h"
/* Recomputes today's reading (astro + tarot + interpretation) from
* `birth`, using the watch's current clock: today's UTC date as the
* tarot seed (same convention as dist/run-engine.sh - see CLAUDE.md's
* "Build & run") and the current moment for the transit snapshot. Safe
* to call again later (e.g. the day rolled over while the app stayed
* open) - overwrites the previous result in place. */
void reading_state_recompute(const BirthData *birth);
/* Valid only after at least one reading_state_recompute() call. */
const DailyReading *reading_state_get_reading(void);
const DailyInterpretation *reading_state_get_interpretation(void);
#endif
+31
View File
@@ -0,0 +1,31 @@
#include "resource_map.h"
#include <pebble.h>
uint32_t card_resource_id(TarotCard card) {
switch (card) {
case CARD_FOOL: return RESOURCE_ID_IMG_CARD_FOOL;
case CARD_MAGICIAN: return RESOURCE_ID_IMG_CARD_MAGICIAN;
case CARD_HIGH_PRIESTESS: return RESOURCE_ID_IMG_CARD_HIGH_PRIESTESS;
case CARD_EMPRESS: return RESOURCE_ID_IMG_CARD_EMPRESS;
case CARD_EMPEROR: return RESOURCE_ID_IMG_CARD_EMPEROR;
case CARD_HIEROPHANT: return RESOURCE_ID_IMG_CARD_HIEROPHANT;
case CARD_LOVERS: return RESOURCE_ID_IMG_CARD_LOVERS;
case CARD_CHARIOT: return RESOURCE_ID_IMG_CARD_CHARIOT;
case CARD_STRENGTH: return RESOURCE_ID_IMG_CARD_STRENGTH;
case CARD_HERMIT: return RESOURCE_ID_IMG_CARD_HERMIT;
case CARD_WHEEL_OF_FORTUNE: return RESOURCE_ID_IMG_CARD_WHEEL_OF_FORTUNE;
case CARD_JUSTICE: return RESOURCE_ID_IMG_CARD_JUSTICE;
case CARD_HANGED_MAN: return RESOURCE_ID_IMG_CARD_HANGED_MAN;
case CARD_DEATH: return RESOURCE_ID_IMG_CARD_DEATH;
case CARD_TEMPERANCE: return RESOURCE_ID_IMG_CARD_TEMPERANCE;
case CARD_DEVIL: return RESOURCE_ID_IMG_CARD_DEVIL;
case CARD_TOWER: return RESOURCE_ID_IMG_CARD_TOWER;
case CARD_STAR: return RESOURCE_ID_IMG_CARD_STAR;
case CARD_MOON: return RESOURCE_ID_IMG_CARD_MOON;
case CARD_SUN: return RESOURCE_ID_IMG_CARD_SUN;
case CARD_JUDGEMENT: return RESOURCE_ID_IMG_CARD_JUDGEMENT;
case CARD_WORLD: return RESOURCE_ID_IMG_CARD_WORLD;
}
return RESOURCE_ID_IMG_CARD_FOOL; /* unreachable for a valid TarotCard */
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef WATCH_RESOURCE_MAP_H
#define WATCH_RESOURCE_MAP_H
#include <stdint.h>
#include "../../../engine/src/tarot.h"
/* Maps a TarotCard to its compiled-in bitmap resource ID. Each of
* watch/package.json's "resources.media" entries is named
* IMG_CARD_<SLUG UPPERCASE>, matching engine/src/tarot_data.c's own
* k_card_slug table (see watch/scripts/gen_card_images.py, which
* generates the underlying PNGs with the same slugs) - Pebble's build
* turns each into a RESOURCE_ID_IMG_CARD_<SLUG> macro in the
* auto-generated resource_ids.auto.h, which this function is the one
* place in the app that switches over. */
uint32_t card_resource_id(TarotCard card);
#endif
+78
View File
@@ -0,0 +1,78 @@
#include "ui_card_window.h"
#include <pebble.h>
#include "resource_map.h"
/* Matches the fixed size gen_card_images.py resizes every card to. */
#define IMAGE_WIDTH 72
#define IMAGE_HEIGHT 120
typedef struct {
TarotCard card;
char *text; /* title + "\n" + body, combined into one scrollable block */
GBitmap *bitmap;
BitmapLayer *bitmap_layer;
ScrollLayer *scroll_layer;
TextLayer *text_layer;
} CardWindowState;
static void window_load(Window *window) {
CardWindowState *state = window_get_user_data(window);
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
state->bitmap = gbitmap_create_with_resource(card_resource_id(state->card));
int image_x = (bounds.size.w - IMAGE_WIDTH) / 2;
state->bitmap_layer = bitmap_layer_create(GRect(image_x, 0, IMAGE_WIDTH, IMAGE_HEIGHT));
bitmap_layer_set_bitmap(state->bitmap_layer, state->bitmap);
bitmap_layer_set_alignment(state->bitmap_layer, GAlignCenter);
layer_add_child(window_layer, bitmap_layer_get_layer(state->bitmap_layer));
GRect scroll_bounds = GRect(0, IMAGE_HEIGHT, bounds.size.w, bounds.size.h - IMAGE_HEIGHT);
state->scroll_layer = scroll_layer_create(scroll_bounds);
scroll_layer_set_click_config_onto_window(state->scroll_layer, window);
state->text_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000));
text_layer_set_font(state->text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text(state->text_layer, state->text);
text_layer_set_overflow_mode(state->text_layer, GTextOverflowModeWordWrap);
GSize content_size = text_layer_get_content_size(state->text_layer);
content_size.w = scroll_bounds.size.w;
content_size.h += 4;
text_layer_set_size(state->text_layer, content_size);
scroll_layer_set_content_size(state->scroll_layer, content_size);
scroll_layer_add_child(state->scroll_layer, text_layer_get_layer(state->text_layer));
layer_add_child(window_layer, scroll_layer_get_layer(state->scroll_layer));
}
static void window_unload(Window *window) {
CardWindowState *state = window_get_user_data(window);
bitmap_layer_destroy(state->bitmap_layer);
gbitmap_destroy(state->bitmap);
text_layer_destroy(state->text_layer);
scroll_layer_destroy(state->scroll_layer);
free(state->text);
free(state);
window_destroy(window);
}
void card_window_push(TarotCard card, const char *title, const char *body) {
CardWindowState *state = malloc(sizeof(CardWindowState));
memset(state, 0, sizeof *state);
state->card = card;
size_t len = strlen(title) + 1 + strlen(body) + 1;
state->text = malloc(len);
snprintf(state->text, len, "%s\n%s", title, body);
Window *window = window_create();
window_set_user_data(window, state);
window_set_window_handlers(window, (WindowHandlers){
.load = window_load,
.unload = window_unload,
});
window_stack_push(window, true);
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef WATCH_UI_CARD_WINDOW_H
#define WATCH_UI_CARD_WINDOW_H
#include "../../../engine/src/tarot.h"
/* Pushes a window showing `card`'s art (via resource_map.h) pinned at
* the top, with `title` and `body` below it as scrollable text (e.g. the
* Celtic Cross position name + orientation, and the position description
* + card meaning). Copies both strings, same as text_window_push(). */
void card_window_push(TarotCard card, const char *title, const char *body);
#endif
+191
View File
@@ -0,0 +1,191 @@
#include "ui_menu_window.h"
#include <pebble.h>
#include "../../../engine/src/i18n.h"
#include "../../../interpreter/src/guidance.h"
#include "../../../interpreter/src/narrative.h"
#include "reading_state.h"
#include "ui_card_window.h"
#include "ui_text_window.h"
typedef enum {
SECTION_TODAY = 0,
SECTION_TRANSITS,
SECTION_CELTIC_CROSS,
NUM_SECTIONS
} MenuSection;
static Window *s_window;
static MenuLayer *s_menu_layer;
/* Small local day-level name table, same duplication policy as
* interpreter/src/main.c's own day_level_name() - see significance.c's
* comment on why each independently-testable/linkable module keeps its
* own copy rather than sharing one across translation units that aren't
* otherwise coupled. */
static const char *day_level_name(DaySignificance level) {
switch (level) {
case DAY_QUIET: return i18n_get("interp.day_level.quiet", "Quiet");
case DAY_NOTABLE: return i18n_get("interp.day_level.notable", "Notable");
case DAY_SIGNIFICANT: return i18n_get("interp.day_level.significant", "Significant");
case DAY_MAJOR: return i18n_get("interp.day_level.major", "Major");
default: return "";
}
}
static const char *reversed_marker(bool reversed) {
return reversed ? i18n_get("ui.reversed", "(Reversed)") : "";
}
static uint16_t get_num_sections_callback(MenuLayer *menu_layer, void *data) {
return NUM_SECTIONS;
}
static uint16_t get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
const DailyInterpretation *interp = reading_state_get_interpretation();
switch (section_index) {
case SECTION_TODAY: return 1;
case SECTION_TRANSITS: return interp->top_item_count > 0 ? interp->top_item_count : 1;
case SECTION_CELTIC_CROSS: return TAROT_SPREAD_SIZE;
default: return 0;
}
}
static int16_t get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
return MENU_CELL_BASIC_HEADER_HEIGHT;
}
static void draw_header_callback(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *data) {
const char *title = "";
switch (section_index) {
case SECTION_TODAY: title = i18n_get("interp.heading_day_significance", "Day Significance"); break;
case SECTION_TRANSITS: title = i18n_get("interp.heading_transits", "Significant Transits"); break;
case SECTION_CELTIC_CROSS: title = i18n_get("ui.celtic_cross", "Celtic Cross"); break;
}
menu_cell_basic_header_draw(ctx, cell_layer, title);
}
static void draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) {
const DailyReading *reading = reading_state_get_reading();
const DailyInterpretation *interp = reading_state_get_interpretation();
char title[48];
char subtitle[64];
switch (cell_index->section) {
case SECTION_TODAY:
snprintf(title, sizeof title, "%s (%d/%d)", day_level_name(interp->day_level),
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
menu_cell_basic_draw(ctx, cell_layer, title, NULL, NULL);
break;
case SECTION_TRANSITS:
if (interp->top_item_count == 0) {
menu_cell_basic_draw(ctx, cell_layer,
i18n_get("interp.no_notable_transits", "No notable transits today."),
NULL, NULL);
break;
}
{
const Aspect *a = &interp->top_items[cell_index->row].aspect;
snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"),
astro_body_name(a->transiting_planet));
snprintf(subtitle, sizeof subtitle, "%s %s %s", astro_aspect_name(a->type),
i18n_get("ui.natal", "natal"), astro_body_name(a->natal_planet));
menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL);
}
break;
case SECTION_CELTIC_CROSS: {
const TarotDraw *draw = &reading->spread.positions[cell_index->row];
snprintf(title, sizeof title, "%s", tarot_position_name((CelticCrossPosition)cell_index->row));
snprintf(subtitle, sizeof subtitle, "%s %s", tarot_card_name(draw->card),
reversed_marker(draw->reversed));
menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL);
break;
}
}
}
static void select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data) {
const DailyReading *reading = reading_state_get_reading();
const DailyInterpretation *interp = reading_state_get_interpretation();
switch (cell_index->section) {
case SECTION_TODAY: {
char body[GUIDANCE_TEXT_MAX];
guidance_write(body, sizeof body, "", interp, &reading->spread);
text_window_push(i18n_get("interp.heading_day_significance", "Day Significance"), body);
break;
}
case SECTION_TRANSITS: {
if (interp->top_item_count == 0) break;
const Aspect *a = &interp->top_items[cell_index->row].aspect;
char title[48];
snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"),
astro_body_name(a->transiting_planet));
char body[NARRATIVE_TEXT_MAX];
narrative_write(body, sizeof body, "", a, reading->transits.bodies[a->transiting_planet].house);
text_window_push(title, body);
break;
}
case SECTION_CELTIC_CROSS: {
const TarotDraw *draw = &reading->spread.positions[cell_index->row];
char title[64];
snprintf(title, sizeof title, "%s: %s %s",
tarot_position_name((CelticCrossPosition)cell_index->row),
tarot_card_name(draw->card), reversed_marker(draw->reversed));
char body[600];
snprintf(body, sizeof body, "%s\n\n%s",
tarot_position_description((CelticCrossPosition)cell_index->row),
tarot_card_meaning(draw->card, draw->reversed));
card_window_push(draw->card, title, body);
break;
}
}
}
static void window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
s_menu_layer = menu_layer_create(bounds);
menu_layer_set_callbacks(s_menu_layer, NULL, (MenuLayerCallbacks){
.get_num_sections = get_num_sections_callback,
.get_num_rows = get_num_rows_callback,
.get_header_height = get_header_height_callback,
.draw_header = draw_header_callback,
.draw_row = draw_row_callback,
.select_click = select_callback,
});
menu_layer_set_click_config_onto_window(s_menu_layer, window);
layer_add_child(window_layer, menu_layer_get_layer(s_menu_layer));
}
static void window_unload(Window *window) {
menu_layer_destroy(s_menu_layer);
s_menu_layer = NULL;
}
void menu_window_push(void) {
if (!s_window) {
s_window = window_create();
window_set_window_handlers(s_window, (WindowHandlers){
.load = window_load,
.unload = window_unload,
});
}
window_stack_push(s_window, true);
}
void menu_window_reload(void) {
if (s_menu_layer) menu_layer_reload_data(s_menu_layer);
}
void menu_window_deinit(void) {
if (s_window) window_destroy(s_window);
s_window = NULL;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef WATCH_UI_MENU_WINDOW_H
#define WATCH_UI_MENU_WINDOW_H
/* The app's root screen: a 3-section MenuLayer (Day Significance /
* Significant Transits / Celtic Cross) reading directly from
* reading_state.h's current reading - see reading.h's own doc comment
* ("walks the returned struct directly to lay out its own screens").
* Selecting a row pushes a detail screen (ui_text_window.h for prose,
* ui_card_window.h for a Celtic Cross position). A persistent singleton -
* created once and left at the bottom of the window stack; call
* menu_window_push() once at startup (and again if the day rolls over
* and the underlying reading changes - the MenuLayer re-reads
* reading_state.h on every redraw, so no explicit refresh call is
* needed beyond menu_layer_reload_data()). */
void menu_window_push(void);
void menu_window_reload(void);
void menu_window_deinit(void);
#endif
+74
View File
@@ -0,0 +1,74 @@
#include "ui_text_window.h"
#include <pebble.h>
#define TITLE_HEIGHT 24
typedef struct {
char *title;
char *body;
TextLayer *title_layer;
ScrollLayer *scroll_layer;
TextLayer *body_layer;
} TextWindowState;
static void window_load(Window *window) {
TextWindowState *state = window_get_user_data(window);
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
state->title_layer = text_layer_create(GRect(0, 0, bounds.size.w, TITLE_HEIGHT));
text_layer_set_font(state->title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text(state->title_layer, state->title);
text_layer_set_overflow_mode(state->title_layer, GTextOverflowModeTrailingEllipsis);
layer_add_child(window_layer, text_layer_get_layer(state->title_layer));
GRect scroll_bounds = GRect(0, TITLE_HEIGHT, bounds.size.w, bounds.size.h - TITLE_HEIGHT);
state->scroll_layer = scroll_layer_create(scroll_bounds);
scroll_layer_set_click_config_onto_window(state->scroll_layer, window);
/* Tall placeholder height so text_layer_get_content_size() below can
* measure the real wrapped height - standard Pebble ScrollLayer
* pattern (a TextLayer taller than its content just clips nothing). */
state->body_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000));
text_layer_set_font(state->body_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text(state->body_layer, state->body);
text_layer_set_overflow_mode(state->body_layer, GTextOverflowModeWordWrap);
GSize content_size = text_layer_get_content_size(state->body_layer);
content_size.w = scroll_bounds.size.w;
content_size.h += 4;
text_layer_set_size(state->body_layer, content_size);
scroll_layer_set_content_size(state->scroll_layer, content_size);
scroll_layer_add_child(state->scroll_layer, text_layer_get_layer(state->body_layer));
layer_add_child(window_layer, scroll_layer_get_layer(state->scroll_layer));
}
static void window_unload(Window *window) {
TextWindowState *state = window_get_user_data(window);
text_layer_destroy(state->title_layer);
text_layer_destroy(state->body_layer);
scroll_layer_destroy(state->scroll_layer);
free(state->title);
free(state->body);
free(state);
window_destroy(window);
}
void text_window_push(const char *title, const char *body) {
TextWindowState *state = malloc(sizeof(TextWindowState));
memset(state, 0, sizeof *state);
state->title = malloc(strlen(title) + 1);
strcpy(state->title, title);
state->body = malloc(strlen(body) + 1);
strcpy(state->body, body);
Window *window = window_create();
window_set_user_data(window, state);
window_set_window_handlers(window, (WindowHandlers){
.load = window_load,
.unload = window_unload,
});
window_stack_push(window, true);
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef WATCH_UI_TEXT_WINDOW_H
#define WATCH_UI_TEXT_WINDOW_H
/* Pushes a scrollable full-screen text window: `title` as a bold header,
* `body` below it, word-wrapped and scrollable with the up/down buttons.
* Used for every screen that's just prose (Summary/Guidance, a single
* transit's narrative) - the Celtic Cross card screens use
* ui_card_window.h instead, which also shows the card's image.
*
* Copies both strings internally (into the pushed window's own heap
* state, freed when it's popped), so the caller's buffer doesn't need to
* outlive the call - every caller so far builds `title`/`body` in a
* short-lived local buffer right before pushing. */
void text_window_push(const char *title, const char *body);
#endif
+5
View File
@@ -0,0 +1,5 @@
// Skeleton only - the real config webview (birth data/language/
// notification settings) is built out in a later task, see CLAUDE.md/
// TaskList. No companion app, no server: this file's only future job is
// showConfiguration/webviewclosed handling for a self-contained `data:`
// URI settings form.
+96
View File
@@ -0,0 +1,96 @@
#
# Pebble app build rules for Deck in a Dash.
#
# Unlike a typical `pebble new-project` skeleton, this app's C sources
# aren't all under src/c/ - most of the actual reading logic lives in
# ../engine/src/ (rng, tarot, tarot_data, astro, i18n, reading) and is
# shared byte-for-byte with the desktop deck-engine/interpreter-cli
# binaries (see the root CLAUDE.md's "Portability to the watch"
# section). build() below explicitly lists the engine source files this
# app needs and glues them onto src/c/**/*.c's own sources - it does NOT
# glob ../engine/src/*.c wholesale, because that directory also contains
# reading.c's desktop-only fprintf-based print functions (compiled out
# under #ifndef PBL_SDK_3, so harmless to include) but must never pull in
# engine/third_party/astronomy/ (the vendored high-precision ephemeris,
# ~127KB compiled - alone bigger than a whole Pebble app's 64KB
# code+data+bss budget). This app uses lowprec_ephemeris.c instead - see
# that file's own header comment.
import os.path
import subprocess
import sys
top = '.'
out = 'build'
# Engine source files this app links in, relative to ../engine/src/ -
# see the module comment above for why this is an explicit list rather
# than a glob.
ENGINE_SRCS = [
'rng.c',
'tarot.c',
'tarot_data.c',
'astro.c',
'i18n.c',
'reading.c',
'lowprec_ephemeris.c',
]
# interpreter/'s significance/narrative/guidance scoring and text - see
# CLAUDE.md's "Interpretation" section. Unlike the desktop interpreter-cli
# (which deliberately keeps these header-only against the engine, for
# independent testability without a real ephemeris - see significance.h's
# own comment), the watch links their real .c implementations straight
# into the same binary as the engine above: there's no deck-engine/
# interpreter-cli JSON pipe to speak of on a single device, just one
# process computing and immediately rendering its own reading. json.c/
# reading_io.c (the JSON parser/loader) are never linked - the watch has
# no JSON to parse, it calls reading_generate() directly.
INTERP_SRCS = [
'significance.c',
'narrative.c',
'guidance.c',
]
def options(ctx):
ctx.load('pebble_sdk')
def configure(ctx):
ctx.load('pebble_sdk')
def build(ctx):
ctx.load('pebble_sdk')
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang, and
# resources/img/*.png from res/img/*.jpeg, before compiling/bundling
# anything - see each script's own doc comment. Both run eagerly (not
# as waf Tasks) since the bundle step below depends on their output
# existing; gen_card_images.py skips any card whose PNG is already
# up to date, so this stays cheap on repeat builds. Requires Pillow
# (`pip install Pillow`) - see watch/README.md.
subprocess.check_call([sys.executable, 'scripts/gen_i18n_tables.py'])
subprocess.check_call([sys.executable, 'scripts/gen_card_images.py'])
engine_dir = ctx.path.parent.find_dir('engine/src')
engine_nodes = [engine_dir.find_node(name) for name in ENGINE_SRCS]
interp_dir = ctx.path.parent.find_dir('interpreter/src')
interp_nodes = [interp_dir.find_node(name) for name in INTERP_SRCS]
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)
app_sources = ctx.path.ant_glob('src/c/**/*.c') + engine_nodes + interp_nodes
ctx.pbl_build(source=app_sources, target=app_elf, bin_type='app')
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']),
js_entry_file='src/pkjs/index.js')