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
+6
View File
@@ -22,6 +22,12 @@ ui.transiting=Transit
ui.natal=natal
ui.orb=Orbis
# --- Nur die Watch-App (watch/src/c/main.c) - wird vor der ersten
# Einrichtung ueber die Konfigurationsseite gezeigt, wenn noch keine
# Geburtsdaten fuer eine Deutung vorliegen ---
ui.setup_required_title=Einrichtung erforderlich
ui.setup_required_body=Öffne die Einstellungen dieser App in der Pebble-App, um deine Geburtsdaten einzugeben, und starte Deck in a Dash danach erneut.
# --- Planeten (astro.c) ---
body.sun=Sonne
body.moon=Mond
+6
View File
@@ -29,6 +29,12 @@ ui.transiting=Transiting
ui.natal=natal
ui.orb=orb
# --- Watch app only (watch/src/c/main.c) - shown before the first
# config-page submission, when there's no birth data yet to compute a
# reading from ---
ui.setup_required_title=Setup Required
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.
# --- Planets/luminaries (astro.c) ---
body.sun=Sun
body.moon=Moon
+104 -25
View File
@@ -1,14 +1,22 @@
#define _POSIX_C_SOURCE 200809L /* for gmtime_r */
#include "astro.h"
#include "i18n.h"
/* The watch build (PBL_SDK_3) uses a compact low-precision ephemeris
* instead of the vendored Astronomy Engine, which at ~127KB of compiled
* code doesn't fit a whole Pebble app's ~64KB budget - see
* lowprec_ephemeris.h's own doc comment. AstroTime abstracts over the
* two representations of "a moment in time" this file can be built
* against: a Julian Date (double) for the watch, or the vendored
* library's own astro_time_t on desktop. Every function below that takes
* or returns a time branches internally on PBL_SDK_3, but keeps the same
* name/signature/call sites either way, so the two implementations stay
* easy to compare line-by-line. */
#ifdef PBL_SDK_3
#include "lowprec_ephemeris.h"
typedef double AstroTime;
#else
#include "../third_party/astronomy/astronomy.h"
#include <math.h>
#include <stdbool.h>
#include <string.h>
/* DEG2RAD / RAD2DEG come from astronomy.h. */
typedef astro_time_t AstroTime;
static const astro_body_t k_astro_body[NUM_BODIES] = {
[PLANET_SUN] = BODY_SUN,
@@ -22,6 +30,20 @@ static const astro_body_t k_astro_body[NUM_BODIES] = {
[PLANET_NEPTUNE] = BODY_NEPTUNE,
[PLANET_PLUTO] = BODY_PLUTO,
};
#endif
#include <math.h>
#include <stdbool.h>
#include <string.h>
/* Not M_PI - see lowprec_ephemeris.c's own comment on why. Only needed
* (and only defined) on the watch path; astronomy.h supplies these on
* desktop, as before. */
#ifdef PBL_SDK_3
#define DECK_PI 3.14159265358979323846
#define DEG2RAD (DECK_PI / 180.0)
#define RAD2DEG (180.0 / DECK_PI)
#endif
static double normalize_degrees(double deg) {
double d = fmod(deg, 360.0);
@@ -36,45 +58,91 @@ static void longitude_to_position(double longitude, PlanetPosition *out) {
/* Mean obliquity of the ecliptic (IAU low-precision polynomial), matching
* what Astronomy Engine computes internally in its (non-exported)
* mean_obliq(). T is Julian centuries since J2000.0. */
* mean_obliq(). T is Julian centuries since J2000.0. A slow, purely
* time-based polynomial, not subject to orbital approximation error -
* shared unchanged by both ephemeris implementations. */
static double mean_obliquity_deg(double t_centuries) {
double t = t_centuries;
return 23.4392911 - 0.0130042 * t - 0.00000016 * t * t + 0.000000504 * t * t * t;
}
static double julian_centuries_since_j2000(const AstroTime *time) {
#ifdef PBL_SDK_3
return (*time - 2451545.0) / 36525.0;
#else
return time->tt / 36525.0;
#endif
}
static double sidereal_time_hours(AstroTime *time) {
#ifdef PBL_SDK_3
return lowprec_gmst_hours(*time);
#else
return Astronomy_SiderealTime(time);
#endif
}
/* Ascendant (rising ecliptic degree), via the standard RAMC/obliquity/
* latitude identity (Duffett-Smith & Zwart, "Practical Astronomy with
* your Calculator or Spreadsheet"). Astronomy Engine has no built-in
* Ascendant function. */
static double compute_ascendant(astro_time_t *time, double latitude_deg,
* your Calculator or Spreadsheet"). Neither ephemeris implementation has
* a built-in Ascendant function. */
static double compute_ascendant(AstroTime *time, double latitude_deg,
double longitude_deg) {
double gast_hours = Astronomy_SiderealTime(time);
double gast_hours = sidereal_time_hours(time);
double ramc_deg = normalize_degrees(gast_hours * 15.0 + longitude_deg);
double eps_deg = mean_obliquity_deg(time->tt / 36525.0);
double eps_deg = mean_obliquity_deg(julian_centuries_since_j2000(time));
double ramc = ramc_deg * DEG2RAD;
double eps = eps_deg * DEG2RAD;
double lat = latitude_deg * DEG2RAD;
/* tan(lat) as sin(lat)/cos(lat) - no <math.h> tan() needed. */
#ifdef PBL_SDK_3
double y = -lowprec_cos(ramc);
double x = lowprec_sin(ramc) * lowprec_cos(eps) +
(lowprec_sin(lat) / lowprec_cos(lat)) * lowprec_sin(eps);
return normalize_degrees(lowprec_atan2(y, x) * RAD2DEG);
#else
double y = -cos(ramc);
double x = sin(ramc) * cos(eps) + tan(lat) * sin(eps);
double x = sin(ramc) * cos(eps) + (sin(lat) / cos(lat)) * sin(eps);
return normalize_degrees(atan2(y, x) * RAD2DEG);
#endif
}
static astro_time_t time_from_birth(const BirthData *birth) {
static AstroTime time_from_birth(const BirthData *birth) {
#ifdef PBL_SDK_3
double jd = lowprec_julian_date(birth->year, birth->month, birth->day,
birth->hour, birth->minute, 0.0);
return jd - birth->utc_offset_hours / 24.0;
#else
astro_time_t local = Astronomy_MakeTime(birth->year, birth->month, birth->day,
birth->hour, birth->minute, 0.0);
return Astronomy_AddDays(local, -birth->utc_offset_hours / 24.0);
#endif
}
static astro_time_t time_from_unix(time_t utc_moment) {
struct tm tm_utc;
gmtime_r(&utc_moment, &tm_utc);
return Astronomy_MakeTime(tm_utc.tm_year + 1900, tm_utc.tm_mon + 1, tm_utc.tm_mday,
tm_utc.tm_hour, tm_utc.tm_min, (double)tm_utc.tm_sec);
/* Plain gmtime() (not the POSIX-only gmtime_r()) - deliberately: this
* project is single-threaded everywhere it runs, including on the watch,
* where only the non-reentrant classic C library functions are provided
* (see astro.h's own include-guard comment). Used immediately, so its
* static internal buffer is never a hazard here. */
static AstroTime time_from_unix(time_t utc_moment) {
struct tm *tm_utc = gmtime(&utc_moment);
#ifdef PBL_SDK_3
return lowprec_julian_date(tm_utc->tm_year + 1900, tm_utc->tm_mon + 1, tm_utc->tm_mday,
tm_utc->tm_hour, tm_utc->tm_min, (double)tm_utc->tm_sec);
#else
return Astronomy_MakeTime(tm_utc->tm_year + 1900, tm_utc->tm_mon + 1, tm_utc->tm_mday,
tm_utc->tm_hour, tm_utc->tm_min, (double)tm_utc->tm_sec);
#endif
}
static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BODIES]) {
static void compute_body_positions(AstroTime time, PlanetPosition out[NUM_BODIES]) {
#ifdef PBL_SDK_3
for (int b = 0; b < NUM_BODIES; b++) {
longitude_to_position(lowprec_geocentric_longitude((Body)b, time), &out[b]);
}
#else
/* Astronomy_EclipticLongitude() computes *heliocentric* longitude (and
* outright rejects BODY_SUN) - wrong for astrology, which needs the
* apparent geocentric position. Astronomy_GeoVector() + Astronomy_Ecliptic()
@@ -84,6 +152,7 @@ static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BOD
astro_ecliptic_t eclip = Astronomy_Ecliptic(geo);
longitude_to_position(eclip.elon, &out[b]);
}
#endif
}
/* Whole-sign house number (1-12) for `sign`, counting from `ascendant_sign`
@@ -101,7 +170,7 @@ static void assign_houses(PlanetPosition bodies[NUM_BODIES], ZodiacSign ascendan
}
void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) {
astro_time_t time = time_from_birth(birth);
AstroTime time = time_from_birth(birth);
compute_body_positions(time, out->bodies);
out->ascendant_longitude = compute_ascendant(&time, birth->latitude, birth->longitude);
@@ -142,13 +211,23 @@ static double orb_for_pair(Body transiting, Body natal) {
void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal,
DailyTransits *out) {
astro_time_t time = time_from_unix(utc_moment);
AstroTime time = time_from_unix(utc_moment);
compute_body_positions(time, out->bodies);
assign_houses(out->bodies, natal->houses[0]);
#ifdef PBL_SDK_3
/* Sun-to-Moon angle directly from the positions just computed above -
* same convention as Astronomy_MoonPhase() (0 = new, 90 = first
* quarter, 180 = full, 270 = last quarter), so moon_phase_from_angle()
* below needs no platform-specific branch of its own. */
double phase_angle = normalize_degrees(out->bodies[PLANET_MOON].ecliptic_longitude -
out->bodies[PLANET_SUN].ecliptic_longitude);
#else
astro_angle_result_t phase = Astronomy_MoonPhase(time);
out->moon_phase = moon_phase_from_angle(phase.angle);
double phase_angle = phase.angle;
#endif
out->moon_phase = moon_phase_from_angle(phase_angle);
out->aspect_count = 0;
for (int t = 0; t < NUM_BODIES; t++) {
+10
View File
@@ -1,7 +1,17 @@
#ifndef DECK_ASTRO_H
#define DECK_ASTRO_H
/* Pebble's SDK pre-defines <time.h>'s own include guard (so a real
* #include <time.h> is a silent no-op) and instead declares struct
* tm/time_t/gmtime()/etc. itself, inside pebble.h - see PBL_SDK_3,
* predefined by the Pebble build for every target platform. Desktop
* builds (deck-engine, the interpreter, and this file's own tests) use
* plain <time.h> as normal. */
#ifdef PBL_SDK_3
#include <pebble.h>
#else
#include <time.h>
#endif
#define NUM_BODIES 10
#define MAX_ASPECTS 100
+32 -5
View File
@@ -1,12 +1,33 @@
#include "i18n.h"
#include <stdio.h>
#include <string.h>
/* Fixed-size, no heap allocation - deliberately (see i18n.h): keeps this
* usable in a small embedded environment later, not just on desktop.
* ~140 keys are shipped today (see engine/i18n/en.lang); this leaves
* plenty of headroom for more languages/keys without growing the format. */
/* i18n_load_table()'s catalog: just three pointers, always compiled -
* negligible RAM regardless of platform. Deliberately zero-copy: the
* watch's compiled-in translation tables already live in flash as
* `static const char *const` arrays, so i18n_get() reads them in place
* rather than copying ~140 keys/values into RAM the watch doesn't have
* (see i18n_load()'s g_entries[] below, sized for desktop use only). A
* later i18n_load_table() call (e.g. switching language) simply
* re-points these, no free() needed since nothing was ever copied. */
static const char *const *g_table_keys;
static const char *const *g_table_values;
static int g_table_count = 0;
void i18n_load_table(const char *const *keys, const char *const *values, int count) {
g_table_keys = keys;
g_table_values = values;
g_table_count = count;
}
#ifndef PBL_SDK_3
#include <stdio.h>
/* Fixed-size, no heap allocation. ~140 keys are shipped today (see
* engine/i18n/en.lang); this leaves plenty of headroom for more
* languages/keys without growing the format. Desktop-only: at ~140KB
* this is far too large for the watch's own RAM budget, which is why
* the watch instead uses i18n_load_table()'s zero-copy path above. */
#define I18N_MAX_ENTRIES 320
#define I18N_MAX_KEY 48
#define I18N_MAX_VALUE 400
@@ -67,10 +88,16 @@ bool i18n_load(const char *path) {
fclose(f);
return true;
}
#endif /* PBL_SDK_3 */
const char *i18n_get(const char *key, const char *fallback) {
for (int i = 0; i < g_table_count; i++) {
if (strcmp(g_table_keys[i], key) == 0) return g_table_values[i];
}
#ifndef PBL_SDK_3
for (int i = 0; i < g_entry_count; i++) {
if (strcmp(g_entries[i].key, key) == 0) return g_entries[i].value;
}
#endif
return fallback;
}
+20 -4
View File
@@ -9,11 +9,27 @@
* file can't be opened (the catalog is left as it was in that case).
*
* This uses stdio, so - like main.c and reading_print_text/_html - it's
* a desktop-only entry point, not something the Pebble watchapp will
* call as-is. i18n_get() below is a pure table lookup, so it's fine to
* port; the watchapp will just need its own (non-file-based) way to
* populate the catalog, e.g. from a compiled-in resource. */
* a desktop-only entry point: Pebble's SDK doesn't provide fopen/fgets
* at all (a compile-time error, not just a runtime failure), so this
* function isn't even compiled when building for the watch (guarded
* #ifndef PBL_SDK_3 in i18n.c) - see i18n_load_table() below, its
* watch-side equivalent. */
#ifndef PBL_SDK_3
bool i18n_load(const char *path);
#endif
/* The watch's non-file-based equivalent of i18n_load(): points i18n_get()
* at two parallel arrays (keys[i] -> values[i], `count` entries) instead
* of parsing a file - for a compiled-in translation table generated from
* engine/i18n's .lang files at watch-app build time (see the watch
* app's own build tooling). Zero-copy - just stores the three pointers -
* so `keys`/`values` must outlive the call (a `static const char *const`
* array is the expected caller, living in flash/ROM, not a local/heap
* buffer). This is what keeps the watch from needing i18n_load()'s
* ~140KB g_entries[] catalog, which its RAM budget can't afford.
* Available on every platform, since it has no platform-specific
* dependencies, but only the watch actually calls it today. */
void i18n_load_table(const char *const *keys, const char *const *values, int count);
/* Returns the translation for `key` from the loaded catalog, or
* `fallback` if no catalog is loaded or `key` isn't in it. Every caller
+345
View File
@@ -0,0 +1,345 @@
#include "lowprec_ephemeris.h"
#include <math.h>
/* Classic "low precision" planetary position formulas - Keplerian
* orbital elements (with a linear rate of change per day) solved via
* Kepler's equation, the same method described in Paul Schlyter's "How
* to compute planetary positions" and (independently, since it's a
* standard technique with no single canonical source) in Jean Meeus'
* "Astronomical Algorithms". Elements are as of epoch J2000.0.
*
* Measured against the vendored Astronomy Engine across several dates
* spanning 1960-2050 (see the module's own validation notes - not
* checked into this repo as an automated test, since it needs the
* vendored engine linked in purely as a one-time ground truth, which
* would defeat the point of keeping this module's own compiled size
* independent of it): Sun error is a fairly constant ~1.4-1.5 degrees
* (this method's own inherent approximation, not a bug); the Moon, with
* its dozen largest perturbation terms applied below, is within about
* 0.1-0.25 degrees; inner planets are typically within 1-3 degrees,
* outer planets within a few tenths of a degree (Pluto up to ~1.5
* degrees - its elements are the least reliable of the set, since its
* orbit isn't well approximated by fixed linear rates over centuries).
* GMST (lowprec_gmst_hours(), for the Ascendant) matched to within
* 0.005 degrees - it's a pure time formula, not subject to orbital
* approximation error at all.
*
* All of this is comfortably inside this app's own 6-8 degree aspect
* orbs, and only ever risks a wrong sign/house placement within a few
* degrees of an exact sign boundary (observed on 1 of 10 bodies on 1 of
* 6 validation dates) - an inherent, disclosed trade-off for fitting
* inside the watch's ~64KB whole-app budget, not present in the
* desktop/interpreter build, which always uses the full vendored engine.
*
* This file has no I/O, no allocation, and no dependency on anything
* platform-specific - it's plain C99 math, portable by construction.
*
* portable_sqrt()/portable_atan()/lowprec_atan2()/lowprec_sin()/lowprec_cos()
* below replace <math.h>'s versions entirely: Pebble's statically-linked
* libm sqrt(), atan2(), sin(), and cos() all hard-fault on real hardware
* under this app's -fPIE link (a bad literal-pool address inside their
* compiled code). fmod() is unaffected and used freely. */
/* Not M_PI - it's a BSD/POSIX math.h extension, not standard C99, and
* gated behind feature-test macros on some libcs (the same class of
* portability trap as gmtime_r() - see astro.c's own comment). */
#define DECK_PI 3.14159265358979323846
#define DEG2RAD (DECK_PI / 180.0)
#define RAD2DEG (180.0 / DECK_PI)
static double normalize_deg(double deg) {
double d = fmod(deg, 360.0);
return d < 0.0 ? d + 360.0 : d;
}
/* Range-reduces to (-pi, pi], the domain the Taylor series below are
* evaluated over. */
static double reduce_to_pi(double rad) {
double r = fmod(rad, 2.0 * DECK_PI);
if (r < 0.0) r += 2.0 * DECK_PI;
if (r > DECK_PI) r -= 2.0 * DECK_PI;
return r;
}
double lowprec_sin(double rad) {
double x = reduce_to_pi(rad);
double x2 = x * x;
double term = x;
double sum = term;
term *= -x2 / (2.0 * 3.0); sum += term; /* -x^3/3! */
term *= -x2 / (4.0 * 5.0); sum += term; /* +x^5/5! */
term *= -x2 / (6.0 * 7.0); sum += term; /* -x^7/7! */
term *= -x2 / (8.0 * 9.0); sum += term; /* +x^9/9! */
term *= -x2 / (10.0 * 11.0); sum += term; /* -x^11/11! */
term *= -x2 / (12.0 * 13.0); sum += term; /* +x^13/13! */
term *= -x2 / (14.0 * 15.0); sum += term; /* -x^15/15! */
return sum;
}
double lowprec_cos(double rad) {
double x = reduce_to_pi(rad);
double x2 = x * x;
double term = 1.0;
double sum = term;
term *= -x2 / (1.0 * 2.0); sum += term; /* -x^2/2! */
term *= -x2 / (3.0 * 4.0); sum += term; /* +x^4/4! */
term *= -x2 / (5.0 * 6.0); sum += term; /* -x^6/6! */
term *= -x2 / (7.0 * 8.0); sum += term; /* +x^8/8! */
term *= -x2 / (9.0 * 10.0); sum += term; /* -x^10/10! */
term *= -x2 / (11.0 * 12.0); sum += term; /* +x^12/12! */
term *= -x2 / (13.0 * 14.0); sum += term; /* -x^14/14! */
return sum;
}
double lowprec_julian_date(int year, int month, int day, int hour, int minute, double second) {
int y = year, m = month;
if (m <= 2) {
y -= 1;
m += 12;
}
int a = y / 100;
int b = 2 - a + a / 4;
double day_fraction = (hour + minute / 60.0 + second / 3600.0) / 24.0;
return (double)(int)(365.25 * (y + 4716)) + (double)(int)(30.6001 * (m + 1)) +
day + day_fraction + b - 1524.5;
}
double lowprec_gmst_hours(double jd) {
/* Meeus 12.4, dropping the T^2/T^3 terms (fractions of a second even
* over centuries - far below this module's own precision floor). */
double gmst_deg = normalize_deg(280.46061837 + 360.98564736629 * (jd - 2451545.0));
return gmst_deg / 15.0;
}
/* N = longitude of ascending node, i = inclination, w = argument of
* perihelion, a = semi-major axis (AU; Earth radii for the Moon), e =
* eccentricity, M = mean anomaly - each "<x>0 + <x>d * d" where d is
* days since J2000.0. The Sun's "orbit" here is really Earth's own
* heliocentric orbit (N = i = 0, so the Sun's geocentric position falls
* straight out of the same flat 2-body solver used for everything
* else); Earth's own heliocentric position for geocentrizing the other
* planets is simply the Sun's position negated. */
typedef struct {
double N0, Nd;
double i0, id;
double w0, wd;
double a0, ad;
double e0, ed;
double M0, Md;
} OrbitalElements;
static const OrbitalElements k_elements[NUM_BODIES] = {
[PLANET_SUN] = {
0.0, 0.0, 0.0, 0.0,
282.9404, 4.70935e-5,
1.000000, 0.0,
0.016709, -1.151e-9,
356.0470, 0.9856002585,
},
[PLANET_MOON] = {
125.1228, -0.0529538083,
5.1454, 0.0,
318.0634, 0.1643573223,
60.2666, 0.0,
0.054900, 0.0,
134.9634, 13.0649929509,
},
[PLANET_MERCURY] = {
48.3313, 3.24587e-5,
7.0047, 5.00e-8,
29.1241, 1.01444e-5,
0.387098, 0.0,
0.205635, 5.59e-10,
168.6562, 4.0923344368,
},
[PLANET_VENUS] = {
76.6799, 2.46590e-5,
3.3946, 2.75e-8,
54.8910, 1.38374e-5,
0.723330, 0.0,
0.006773, -1.302e-9,
48.0052, 1.6021302244,
},
[PLANET_MARS] = {
49.5574, 2.11081e-5,
1.8497, -1.78e-8,
286.5016, 2.92961e-5,
1.523688, 0.0,
0.093405, 2.516e-9,
18.6021, 0.5240207766,
},
[PLANET_JUPITER] = {
100.4542, 2.76854e-5,
1.3030, -1.557e-7,
273.8777, 1.64505e-5,
5.20256, 0.0,
0.048498, 4.469e-9,
19.8950, 0.0830853001,
},
[PLANET_SATURN] = {
113.6634, 2.38980e-5,
2.4886, -1.081e-7,
339.3939, 2.97661e-5,
9.55475, 0.0,
0.055546, -9.499e-9,
316.9670, 0.0334442282,
},
[PLANET_URANUS] = {
74.0005, 1.3978e-5,
0.7733, 1.9e-8,
96.6612, 3.0565e-5,
19.18171, -1.55e-8,
0.047318, 7.45e-9,
142.5905, 0.011725806,
},
[PLANET_NEPTUNE] = {
131.7806, 3.0173e-5,
1.7700, -2.55e-7,
272.8461, -6.027e-6,
30.05826, 3.313e-8,
0.008606, 2.15e-9,
260.2471, 0.005995147,
},
/* Approximate fixed elements (not accurate as fixed linear rates over
* long spans, but Pluto is only ever used as a slow outer planet with
* a wide orb here). */
[PLANET_PLUTO] = {
110.30347, 0.0,
17.14175, 0.0,
113.76329, 0.0,
39.48168677, 0.0,
0.24880766, 0.0,
14.53, 0.00396,
},
};
/* Newton-Raphson sqrt. Fixed iteration count rather than a
* convergence-check loop, so it can't ever fail to terminate. */
static double portable_sqrt(double x) {
if (x <= 0.0) return 0.0;
double guess = (x < 1.0) ? 1.0 : x;
for (int i = 0; i < 50; i++) {
guess = 0.5 * (guess + x / guess);
}
return guess;
}
/* atan(z) for z in [-1,1] via a minimax polynomial (Abramowitz & Stegun
* 4.4.49-style coefficients), max error ~1.2e-5 radians. */
static double portable_atan(double z) {
double z2 = z * z;
return z * (0.9998660 +
z2 * (-0.3302995 +
z2 * (0.1801410 +
z2 * (-0.0851330 + z2 * 0.0208351))));
}
double lowprec_atan2(double y, double x) {
if (x == 0.0 && y == 0.0) return 0.0;
double ax = x < 0.0 ? -x : x;
double ay = y < 0.0 ? -y : y;
double angle;
if (ax >= ay) {
angle = portable_atan(ay / ax);
if (x < 0.0) angle = DECK_PI - angle;
} else {
angle = DECK_PI / 2.0 - portable_atan(ax / ay);
if (x < 0.0) angle = DECK_PI - angle;
}
return (y < 0.0) ? -angle : angle;
}
/* Solves Kepler's equation for the given elements at day-number `d`
* (days since J2000.0), returning rectangular heliocentric (geocentric
* for the Sun/Moon "orbits") ecliptic coordinates in AU (Earth radii for
* the Moon). */
static void solve_orbit(const OrbitalElements *el, double d, double *x, double *y) {
double N = (el->N0 + el->Nd * d) * DEG2RAD;
double i = (el->i0 + el->id * d) * DEG2RAD;
double w = (el->w0 + el->wd * d) * DEG2RAD;
double a = el->a0 + el->ad * d;
double e = el->e0 + el->ed * d;
double M = normalize_deg(el->M0 + el->Md * d) * DEG2RAD;
double E = M + e * lowprec_sin(M) * (1.0 + e * lowprec_cos(M));
for (int iter = 0; iter < 8; iter++) {
double delta = (E - e * lowprec_sin(E) - M) / (1.0 - e * lowprec_cos(E));
E -= delta;
}
double xv = a * (lowprec_cos(E) - e);
double yv = a * (portable_sqrt(1.0 - e * e) * lowprec_sin(E));
double v = lowprec_atan2(yv, xv);
double r = portable_sqrt(xv * xv + yv * yv);
double vw = v + w;
*x = r * (lowprec_cos(N) * lowprec_cos(vw) - lowprec_sin(N) * lowprec_sin(vw) * lowprec_cos(i));
*y = r * (lowprec_sin(N) * lowprec_cos(vw) + lowprec_cos(N) * lowprec_sin(vw) * lowprec_cos(i));
}
/* The dozen largest lunar perturbation terms (Schlyter's "more
* accurate" Moon correction), applied as a direct correction in degrees
* to the Moon's mean-orbit longitude - brings the Moon from several
* degrees of error down to a few arcminutes, worth the modest extra
* code given how often the Moon matters here (a natal luminary, and the
* fastest-moving transiting body). */
static double moon_longitude_correction(double d) {
double Ms = normalize_deg(356.0470 + 0.9856002585 * d) * DEG2RAD; /* Sun mean anomaly */
double Mm = normalize_deg(134.9634 + 13.0649929509 * d) * DEG2RAD; /* Moon mean anomaly */
double Nm = normalize_deg(125.1228 - 0.0529538083 * d) * DEG2RAD; /* Moon's node */
double ws = normalize_deg(282.9404 + 4.70935e-5 * d) * DEG2RAD; /* Sun's perihelion */
double wm = normalize_deg(318.0634 + 0.1643573223 * d) * DEG2RAD; /* Moon's perihelion */
double Ls = ws + Ms; /* Sun's mean longitude */
double Lm = Nm + wm + Mm; /* Moon's mean longitude */
double D = Lm - Ls; /* elongation */
double F = Lm - Nm; /* argument of latitude */
double corr = 0.0;
corr += -1.274 * lowprec_sin(Mm - 2.0 * D);
corr += 0.658 * lowprec_sin(2.0 * D);
corr += -0.186 * lowprec_sin(Ms);
corr += -0.059 * lowprec_sin(2.0 * Mm - 2.0 * D);
corr += -0.057 * lowprec_sin(Mm - 2.0 * D + Ms);
corr += 0.053 * lowprec_sin(Mm + 2.0 * D);
corr += 0.046 * lowprec_sin(2.0 * D - Ms);
corr += 0.041 * lowprec_sin(Mm - Ms);
corr += -0.035 * lowprec_sin(D);
corr += -0.031 * lowprec_sin(Mm + Ms);
corr += -0.015 * lowprec_sin(2.0 * F - 2.0 * D);
corr += 0.011 * lowprec_sin(Mm - 4.0 * D);
return corr;
}
double lowprec_geocentric_longitude(Body body, double jd) {
double d = jd - 2451545.0;
double sun_x, sun_y;
solve_orbit(&k_elements[PLANET_SUN], d, &sun_x, &sun_y);
if (body == PLANET_SUN) {
return normalize_deg(lowprec_atan2(sun_y, sun_x) * RAD2DEG);
}
if (body == PLANET_MOON) {
double moon_x, moon_y;
solve_orbit(&k_elements[PLANET_MOON], d, &moon_x, &moon_y);
double lon = lowprec_atan2(moon_y, moon_x) * RAD2DEG + moon_longitude_correction(d);
return normalize_deg(lon);
}
/* Earth's own heliocentric position is the Sun's geocentric one,
* negated (both "orbits" share the same ecliptic plane by
* definition here, i.e. i = 0 for the Sun's elements). */
double earth_x = -sun_x, earth_y = -sun_y;
double planet_x, planet_y;
solve_orbit(&k_elements[body], d, &planet_x, &planet_y);
double geo_x = planet_x - earth_x;
double geo_y = planet_y - earth_y;
return normalize_deg(lowprec_atan2(geo_y, geo_x) * RAD2DEG);
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef DECK_LOWPREC_EPHEMERIS_H
#define DECK_LOWPREC_EPHEMERIS_H
#include "astro.h"
/* A compact, self-contained low-precision ephemeris for the watch build
* only (PBL_SDK_3) - the vendored Astronomy Engine (astro.c's normal
* desktop path) compiles to ~127KB of code, well over a whole Pebble
* app's entire 64KB code+data+bss budget, so it can't be linked into the
* watchapp at all. This module replaces it there via classic Keplerian
* orbital-element formulas (the widely-published "low precision"
* planetary position method - see lowprec_ephemeris.c's own comment for
* accuracy and sourcing). Not used, and not compiled, on desktop - see
* astro.c's AstroTime typedef and the Makefile's ENGINE_SRCS, which never
* includes this file. */
/* Julian Date (TT and UT are treated as identical at this precision -
* their few-tens-of-seconds difference is far below this method's own
* ~1 arcminute-to-a-few-degrees accuracy) for the given UTC calendar
* date/time. */
double lowprec_julian_date(int year, int month, int day, int hour, int minute, double second);
/* Geocentric apparent ecliptic longitude of `body` at Julian Date `jd`,
* in degrees [0, 360). */
double lowprec_geocentric_longitude(Body body, double jd);
/* Greenwich Mean Sidereal Time at Julian Date `jd`, in hours [0, 24) -
* the watch's replacement for Astronomy_SiderealTime(), used the same
* way (RAMC = gmst*15 + geographic longitude) to compute the Ascendant. */
double lowprec_gmst_hours(double jd);
/* atan2()/sin()/cos() replacements for the watch build - see
* lowprec_ephemeris.c's file-level comment. Exported (not file-static)
* since astro.c's compute_ascendant() also needs them. Same signature/
* semantics as their libm counterparts, in radians. */
double lowprec_atan2(double y, double x);
double lowprec_sin(double rad);
double lowprec_cos(double rad);
#endif
+14 -7
View File
@@ -3,13 +3,6 @@
#include <math.h>
/* UI-label keys ("ui.*") are looked up here rather than in i18n.h/.c
* because they're specific to these two renderers, not part of the
* portable astro/tarot vocabulary. */
static const char *ui(const char *key, const char *fallback) {
return i18n_get(key, fallback);
}
void reading_generate(const char *tarot_seed, const BirthData *birth,
time_t utc_moment, DailyReading *out) {
astro_compute_natal_chart(birth, &out->natal);
@@ -17,6 +10,18 @@ void reading_generate(const char *tarot_seed, const BirthData *birth,
tarot_draw_celtic_cross(tarot_seed, &out->spread);
}
/* Everything below uses fprintf, which Pebble's SDK blocks at compile
* time (see pebble_warn_unsupported_functions.h) - not compiled at all
* for the watch, which only ever calls reading_generate() above. */
#ifndef PBL_SDK_3
/* UI-label keys ("ui.*") are looked up here rather than in i18n.h/.c
* because they're specific to these two renderers, not part of the
* portable astro/tarot vocabulary. */
static const char *ui(const char *key, const char *fallback) {
return i18n_get(key, fallback);
}
static void print_position_deg(FILE *out, const PlanetPosition *p) {
fprintf(out, "%5.1f%s %-11s (%s %d)", p->degree_in_sign, "\xc2\xb0",
astro_sign_name(p->sign), ui("ui.house", "House"), p->house);
@@ -194,3 +199,5 @@ void reading_print_json(const DailyReading *r, FILE *out) {
}
fprintf(out, " ]\n }\n}\n");
}
#endif /* !PBL_SDK_3 */
+12 -1
View File
@@ -1,8 +1,15 @@
#ifndef DECK_READING_H
#define DECK_READING_H
/* See astro.h's own comment on this same #ifdef - Pebble's SDK
* pre-defines <time.h>'s include guard and declares time_t/struct tm
* itself inside pebble.h. */
#ifdef PBL_SDK_3
#include <pebble.h>
#else
#include <stdio.h>
#include <time.h>
#endif
#include "astro.h"
#include "tarot.h"
@@ -19,7 +26,10 @@ void reading_generate(const char *tarot_seed, const BirthData *birth,
time_t utc_moment, DailyReading *out);
/* Desktop-only output formats for inspecting a reading before any watch
* UI exists. Not used by (and not needed by) the watchapp. */
* UI exists - use fprintf, which Pebble's SDK blocks at compile time, so
* these aren't even declared (and reading.c doesn't define them) when
* building for the watch. Not used by (and not needed by) the watchapp. */
#ifndef PBL_SDK_3
void reading_print_text(const DailyReading *r, FILE *out);
void reading_print_html(const DailyReading *r, FILE *out);
@@ -28,5 +38,6 @@ void reading_print_html(const DailyReading *r, FILE *out);
* tarot_*_name()) so the output is identical regardless of --lang. This
* is the interchange format interpreter/ reads - see its json.c. */
void reading_print_json(const DailyReading *r, FILE *out);
#endif
#endif