Add daily notification wakeups and a make watchapp packaging target
build / build (push) Successful in 20s
build / build (push) Successful in 20s
Schedules a one-shot Pebble wakeup for the configured notification time, re-arming it on every launch and config change, and vibrates/ refreshes the report when it fires (foreground or fresh launch). Also adds a `make watchapp` target that builds the watchapp via the Pebble SDK and copies a versioned .pbw to dist/, matching `make package`'s version resolution — deliberately not wired into CI, which has no Pebble SDK installed.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,9 @@ make # builds dist/{deck-engine,interpreter-cli}, dist/img/, dist/i18n/
|
|||||||
make test # builds and runs engine/tests/smoke_test.c and interpreter/tests/*_test.c
|
make test # builds and runs engine/tests/smoke_test.c and interpreter/tests/*_test.c
|
||||||
make clean # removes build/ and the generated binaries/img/i18n (never touches dist/user.properties)
|
make clean # removes build/ and the generated binaries/img/i18n (never touches dist/user.properties)
|
||||||
make package # builds a versioned Linux CLI tarball at dist/ - see "Packaging" below
|
make package # builds a versioned Linux CLI tarball at dist/ - see "Packaging" below
|
||||||
|
make watchapp # builds the Pebble watchapp and copies a versioned .pbw to
|
||||||
|
# dist/ - see "Packaging" below. Requires the Pebble SDK
|
||||||
|
# (not needed for anything else above); not run by CI.
|
||||||
```
|
```
|
||||||
|
|
||||||
A single `Makefile` at the repo root builds both `engine/` and
|
A single `Makefile` at the repo root builds both `engine/` and
|
||||||
@@ -101,6 +104,22 @@ published over SFTP to `dl.ladkau.de` under `files/deck-in-a-dash/` -
|
|||||||
requires a `DL_SFTP_KEY` secret configured on this repo (or inherited
|
requires a `DL_SFTP_KEY` secret configured on this repo (or inherited
|
||||||
from the org/instance level).
|
from the org/instance level).
|
||||||
|
|
||||||
|
### Watch app packaging (`make watchapp`)
|
||||||
|
|
||||||
|
A separate target, deliberately **not** a prerequisite of `all` and
|
||||||
|
**not** run by CI - it shells out to `pebble build` inside `watch/`
|
||||||
|
(via `watch/wscript`'s own Pebble SDK/waf tooling, not this Makefile's
|
||||||
|
plain-`cc` rules) and copies the resulting `watch/build/watch.pbw` to
|
||||||
|
`dist/deck-in-a-dash-<version>.pbw`, using the exact same version
|
||||||
|
resolution as `make package` above (current git tag, or `make watchapp
|
||||||
|
VERSION=1.2.3` to override - see that section, not duplicated here).
|
||||||
|
Requires the Pebble SDK (`pebble` on `PATH`, plus the Python 3 + Pillow
|
||||||
|
`watch/wscript` needs for its resource-generation scripts) - a machine
|
||||||
|
without it can still build/test/package the CLI via every other target
|
||||||
|
in this file. Not wired into `.gitea/workflows/build.yml`: doing so
|
||||||
|
would need the Pebble SDK toolchain installed on the CI runner, which
|
||||||
|
this project doesn't currently provision.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ GUIDANCE_TEST_BIN := $(BUILD_DIR)/guidance_test
|
|||||||
ARCH := $(shell uname -m)
|
ARCH := $(shell uname -m)
|
||||||
PACKAGE_DIR := $(BUILD_DIR)/package
|
PACKAGE_DIR := $(BUILD_DIR)/package
|
||||||
|
|
||||||
.PHONY: all test clean images i18n scripts package
|
# ---- watch app (Pebble) ----
|
||||||
|
|
||||||
|
WATCH_DIR := watch
|
||||||
|
WATCH_PBW := $(WATCH_DIR)/build/watch.pbw
|
||||||
|
|
||||||
|
.PHONY: all test clean images i18n scripts package watchapp
|
||||||
|
|
||||||
all: $(ENGINE_BINARY) $(INTERP_BINARY) images i18n scripts
|
all: $(ENGINE_BINARY) $(INTERP_BINARY) images i18n scripts
|
||||||
|
|
||||||
@@ -208,3 +213,32 @@ clean:
|
|||||||
rm -rf $(BUILD_DIR) $(ENGINE_BINARY) $(INTERP_BINARY) $(DIST_IMG_DIR) $(DIST_I18N_DIR) \
|
rm -rf $(BUILD_DIR) $(ENGINE_BINARY) $(INTERP_BINARY) $(DIST_IMG_DIR) $(DIST_I18N_DIR) \
|
||||||
$(DIST_DIR)/run-engine.sh $(DIST_DIR)/run-interpreter.sh
|
$(DIST_DIR)/run-engine.sh $(DIST_DIR)/run-interpreter.sh
|
||||||
# user.properties holds the user's own data - never removed by clean.
|
# user.properties holds the user's own data - never removed by clean.
|
||||||
|
|
||||||
|
# Builds the Pebble watchapp (`pebble build` in watch/, via its own
|
||||||
|
# wscript/pebble_sdk waf tooling - not a plain C toolchain, so this is a
|
||||||
|
# separate target from `all` rather than one of its prerequisites) and
|
||||||
|
# copies the resulting .pbw into dist/, versioned the same way `package`
|
||||||
|
# above versions the CLI tarball (current git tag, or
|
||||||
|
# `make watchapp VERSION=1.2.3` to override). Requires the Pebble SDK
|
||||||
|
# (`pebble` on PATH, plus Python 3 + Pillow for watch/scripts/*.py - see
|
||||||
|
# watch/wscript) - this is NOT checked by the root `make`/`make test`,
|
||||||
|
# so a machine without the Pebble SDK can still build/test the CLI.
|
||||||
|
watchapp:
|
||||||
|
cd $(WATCH_DIR) && pebble build
|
||||||
|
@mkdir -p $(DIST_DIR)
|
||||||
|
@V="$(VERSION)"; \
|
||||||
|
if [ -z "$$V" ]; then \
|
||||||
|
if TAG=$$(git describe --tags --exact-match --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null); then \
|
||||||
|
V=$${TAG#v}; \
|
||||||
|
else \
|
||||||
|
V="0.0.0-dev+$$(git rev-parse --short HEAD)"; \
|
||||||
|
echo "WARNING: HEAD is not on a vX.Y.Z tag - building placeholder version $$V (push a tag to drive a real release version)" >&2; \
|
||||||
|
fi; \
|
||||||
|
fi; \
|
||||||
|
case "$$V" in \
|
||||||
|
[0-9]*.[0-9]*.[0-9]*) ;; \
|
||||||
|
*) echo "PREFLIGHT FAIL: VERSION '$$V' is not a semantic version (expected X.Y.Z, optionally with a -pre+meta suffix)" >&2; exit 1;; \
|
||||||
|
esac; \
|
||||||
|
PBW_NAME="deck-in-a-dash-$$V.pbw"; \
|
||||||
|
cp $(WATCH_PBW) "$(DIST_DIR)/$$PBW_NAME"; \
|
||||||
|
echo "Wrote $(DIST_DIR)/$$PBW_NAME"
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ interpreter/ Separate module + binary (dist/interpreter-cli)
|
|||||||
deck-engine's --format json output. Supports
|
deck-engine's --format json output. Supports
|
||||||
--format text|html|json and --lang en|de for
|
--format text|html|json and --lang en|de for
|
||||||
its own output too (see CLAUDE.md).
|
its own output too (see CLAUDE.md).
|
||||||
|
watch/ The actual Pebble watchapp - links engine/src/
|
||||||
|
and interpreter/src/ in directly (see CLAUDE.md's
|
||||||
|
"Portability to the watch"), plus its own
|
||||||
|
persistence, reading recompute, UI, daily
|
||||||
|
notification, and config page. Built separately
|
||||||
|
from the rest of this repo (`make watchapp`,
|
||||||
|
below) - requires the Pebble SDK.
|
||||||
docs/ Architecture/dataflow diagrams, input/output format reference.
|
docs/ Architecture/dataflow diagrams, input/output format reference.
|
||||||
Makefile Single root Makefile, builds both engine/ and
|
Makefile Single root Makefile, builds both engine/ and
|
||||||
interpreter/, and packages a release (`make package`).
|
interpreter/, and packages a release (`make package`).
|
||||||
@@ -78,6 +85,15 @@ make package # -> dist/deck-in-a-dash-<version>-linux-<arch>.tar.gz, a
|
|||||||
|
|
||||||
Requires only a C99 compiler and `make` — no other dependencies.
|
Requires only a C99 compiler and `make` — no other dependencies.
|
||||||
|
|
||||||
|
**Building the watch app itself** (`watch/`, the actual Pebble app)
|
||||||
|
needs the separate Pebble SDK (`pebble` on `PATH`), so it's a separate
|
||||||
|
target, not part of `make`/`make test`/`make package` above:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make watchapp # -> dist/deck-in-a-dash-<version>.pbw, same version
|
||||||
|
# resolution as `make package`
|
||||||
|
```
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
**Daily use**, via `dist/run-engine.sh`: pulls today's date and the
|
**Daily use**, via `dist/run-engine.sh`: pulls today's date and the
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@
|
|||||||
#include "app_message.h"
|
#include "app_message.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "i18n_tables.h"
|
#include "i18n_tables.h"
|
||||||
|
#include "notify.h"
|
||||||
#include "reading_state.h"
|
#include "reading_state.h"
|
||||||
#include "ui_report_window.h"
|
#include "ui_report_window.h"
|
||||||
#include "ui_text_window.h"
|
#include "ui_text_window.h"
|
||||||
@@ -26,6 +27,7 @@ static void on_config_updated(const WatchConfig *cfg) {
|
|||||||
|
|
||||||
config_log(&s_config); /* the values this newly-recomputed reading is based on */
|
config_log(&s_config); /* the values this newly-recomputed reading is based on */
|
||||||
reading_state_recompute(&s_config.birth, s_config.gender);
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
|
notify_reschedule(&s_config); /* settings may have changed notify_enabled/hour/minute */
|
||||||
if (was_configured) {
|
if (was_configured) {
|
||||||
report_window_reload();
|
report_window_reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -33,16 +35,36 @@ static void on_config_updated(const WatchConfig *cfg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fires only if the scheduled wakeup goes off while this app is already
|
||||||
|
* open (notify_was_wakeup_launch() below covers the far more common
|
||||||
|
* case: the wakeup launching the app fresh). Refreshes the same way a
|
||||||
|
* new day's config submission would, since the report window is already
|
||||||
|
* on screen and the reading may now be for a new day. */
|
||||||
|
static void on_wakeup(WakeupId wakeup_id, int32_t cookie) {
|
||||||
|
(void)wakeup_id;
|
||||||
|
(void)cookie;
|
||||||
|
vibes_double_pulse();
|
||||||
|
if (!s_config.configured) return;
|
||||||
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
|
report_window_reload();
|
||||||
|
notify_reschedule(&s_config); /* wakeup_schedule() is one-shot - re-arm for tomorrow */
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
config_load(&s_config);
|
config_load(&s_config);
|
||||||
watch_i18n_load(s_config.lang);
|
watch_i18n_load(s_config.lang);
|
||||||
app_message_init(on_config_updated);
|
app_message_init(on_config_updated);
|
||||||
|
notify_subscribe(on_wakeup);
|
||||||
|
|
||||||
config_log(&s_config); /* the values this session's reading (if any) is based on */
|
config_log(&s_config); /* the values this session's reading (if any) is based on */
|
||||||
|
|
||||||
if (s_config.configured) {
|
if (s_config.configured) {
|
||||||
reading_state_recompute(&s_config.birth, s_config.gender);
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
report_window_push();
|
report_window_push();
|
||||||
|
notify_reschedule(&s_config); /* wakeup_schedule() is one-shot - re-arm on every launch */
|
||||||
|
if (notify_was_wakeup_launch()) {
|
||||||
|
vibes_double_pulse();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
show_setup_required();
|
show_setup_required();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include "notify.h"
|
||||||
|
|
||||||
|
/* Only one kind of wakeup event exists in this app, so the cookie value
|
||||||
|
* itself carries no information - it exists only because
|
||||||
|
* wakeup_schedule()/wakeup_get_launch_event() require one. */
|
||||||
|
#define NOTIFY_WAKEUP_COOKIE 0
|
||||||
|
|
||||||
|
void notify_reschedule(const WatchConfig *cfg) {
|
||||||
|
wakeup_cancel_all();
|
||||||
|
if (!cfg->notify_enabled) return;
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm target_tm = *localtime(&now);
|
||||||
|
target_tm.tm_hour = cfg->notify_hour;
|
||||||
|
target_tm.tm_min = cfg->notify_minute;
|
||||||
|
target_tm.tm_sec = 0;
|
||||||
|
|
||||||
|
time_t target = mktime(&target_tm);
|
||||||
|
if (target <= now) target += SECONDS_PER_DAY; /* today's time already passed */
|
||||||
|
|
||||||
|
WakeupId id = wakeup_schedule(target, NOTIFY_WAKEUP_COOKIE, true);
|
||||||
|
if (id < 0) {
|
||||||
|
APP_LOG(APP_LOG_LEVEL_ERROR, "notify: wakeup_schedule failed: %d", (int)id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notify_was_wakeup_launch(void) {
|
||||||
|
return launch_reason() == APP_LAUNCH_WAKEUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify_subscribe(WakeupHandler handler) {
|
||||||
|
wakeup_service_subscribe(handler);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef WATCH_NOTIFY_H
|
||||||
|
#define WATCH_NOTIFY_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
/* (Re)schedules the daily notification wakeup from `cfg`'s notify_*
|
||||||
|
* fields: cancels any wakeup this app previously scheduled, then - if
|
||||||
|
* cfg->notify_enabled - schedules a new one-shot wakeup_schedule() for
|
||||||
|
* the next occurrence of notify_hour:notify_minute in the watch's own
|
||||||
|
* local time (today if that time hasn't passed yet, tomorrow otherwise).
|
||||||
|
* If !cfg->notify_enabled, this only cancels - no new wakeup is
|
||||||
|
* scheduled.
|
||||||
|
*
|
||||||
|
* Safe to call any time, and needs to be: once per app launch (Pebble's
|
||||||
|
* wakeup_schedule() is one-shot - each firing consumes itself, so the
|
||||||
|
* next day's has to be scheduled again after it fires or after a wakeup
|
||||||
|
* that arrives while the app happens to already be open - see
|
||||||
|
* notify_subscribe()) and on every config submission (settings may have
|
||||||
|
* changed notify_enabled/notify_hour/notify_minute). */
|
||||||
|
void notify_reschedule(const WatchConfig *cfg);
|
||||||
|
|
||||||
|
/* True if this app launch was triggered by the daily notification wakeup
|
||||||
|
* firing rather than the user/phone opening it normally - main.c uses
|
||||||
|
* this to vibrate on launch, since the report window it pushes either
|
||||||
|
* way already leads with day significance and the top transit (the
|
||||||
|
* "quick digest" this notification promises), so no separate screen is
|
||||||
|
* needed for it. */
|
||||||
|
bool notify_was_wakeup_launch(void);
|
||||||
|
|
||||||
|
/* Subscribes `handler` to fire if the scheduled wakeup goes off while
|
||||||
|
* this app is already running in the foreground - the only case
|
||||||
|
* notify_was_wakeup_launch() can't see, since no fresh launch happens
|
||||||
|
* then (see wakeup_service_subscribe()'s own doc comment in pebble.h).
|
||||||
|
* Call once, early in main(). */
|
||||||
|
void notify_subscribe(WakeupHandler handler);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user