Introduces watch/, a real Pebble watchapp (built via pebble build) that
build / build (push) Successful in 30s
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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++) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user