Skip to content
Cvar1984Public

Latest commit

 

History

26 Commits

Folders and files

Repository files navigation

Miniverse

A sky pointer for Garmin watches. Pick an object (the Sun, the Moon, a planet, a bright star), hold the watch up with the back facing the sky, and the object is drawn where it really is behind the case. Move your arm until the marker settles in the middle of the screen and you are looking straight at it.

It is the idea behind a phone astronomy app held up with its camera, on a wrist, with no network, no phone, and no chart to orient yourself against first.

version 1

Contents

How it works

The aim axis is the watch's Z axis, straight out through the back of the case, not the 12 o'clock edge. That makes the watch a viewfinder: you look at the screen, the sky is behind it, and the object is drawn at the place on the glass you would see it through if the watch were transparent.

Because it is a window, turning your wrist turns what you see through it. The picture is built in the watch's own frame and rolls with your hand. The written turn/tilt guidance underneath is measured against gravity instead, because it describes how to swing your arm, which has nothing to do with how the watch is rotated in your grip.

Two independent chains meet at the projection, and the whole thing runs once per frame at 10 Hz:

flowchart TD
    subgraph SKY["Where the object is"]
        direction TB
        CLK["Clock, UTC"] --> JD["Julian Day<br/>64-bit"]
        JD --> LST["Local sidereal time<br/>GMST plus longitude"]
        JD --> SER["Series and elements<br/>Sun, Moon, planets"]
        CAT["Star catalogue<br/>J2000"] --> RADEC
        SER --> RADEC["Right ascension<br/>and declination"]
        LST --> ALTAZ["Altitude and azimuth"]
        RADEC --> ALTAZ
        LAT["Latitude, from GPS"] --> ALTAZ
        ALTAZ --> CORR["Parallax then refraction"]
        CORR --> ENU["Unit vector<br/>east, north, up"]
    end

    subgraph WATCH["Where the watch is pointing"]
        direction TB
        ACC["Accelerometer"] --> GRAV["Gravity, normalised<br/>and negated"]
        MAG["Magnetometer"] --> NORTH["Magnetic north<br/>vertical part removed"]
        GRAV --> NORTH
        GRAV --> FRAME["Device frame<br/>world axes in watch coordinates"]
        NORTH --> FRAME
    end

    ENU --> OFF["Rotate into watch axes<br/>right, up, forward"]
    FRAME --> OFF
    OFF --> Q{"forward greater<br/>than 0.1?"}
    Q -->|no| DROP["Behind the watch.<br/>Marker pinned to the rim,<br/>grid line broken"]
    Q -->|yes| PROJ["Perspective divide"]
    PROJ --> XY["Screen x, y"]

    style SKY fill:#0d1117,stroke:#30363d
    style WATCH fill:#0d1117,stroke:#30363d
Loading

Both sensor readings are smoothed as vectors before any of this, never as angles (see Smoothing).

Screens

flowchart LR
    ROOT["Locate Sky Object"]
    ROOT --> ALL["Show All"]
    ROOT --> SUN["Sun"]
    ROOT --> MOON["Moon"]
    ROOT --> PLIST["Planets list"]
    ROOT --> SLIST["Stars list"]
    PLIST --> AIM
    SLIST --> AIM
    SUN --> AIM["Sky screen"]
    MOON --> AIM
    ALL --> AIM
    AIM -->|hold up / menu| SET["Settings"]
    SET -->|back| AIM
    AIM -->|back| ROOT
Loading

Root menu

Aiming at one object

Moon with phase, locked on

The object name at the top, the marker where the object is, and a table below: azimuth and altitude for the object (Obj) and for the aim (Aim), then how far to Turn and Tilt. The two rows converge as you settle onto the object. If a sensor axis is wired the wrong way they move apart as you close in, so the fault shows at once.

Within $8^\circ$ of the object the marker takes a green ring and the guidance is replaced by On target. Below the horizon the altitude reads red: the object is placed correctly, but it is underneath you.

When the object is off the edge of the view it pins to the rim with a chevron pointing further the way to move, inset far enough that the marker and its chevron both stay on the glass.

Show All

Show All with both grids

Above: both grids on at $60^\circ$, crossing each other, with N at the north point.

This mode draws the whole catalogue as a plain sky map: the Sun, the Moon, the planets and the bright stars. Nothing is being aimed at, so there is no turn/tilt guidance, only a readout of where the watch is pointing.

Nothing is pinned to the rim in this mode, because with the whole sky on show the markers would pile up around the edge. An object that is not in front of the watch is not drawn.

With Crosshair on, a small cross marks the middle of the screen, and the object nearest it, within the same $8^\circ$ as On target, is named at the top with its magnitude at the bottom. Star magnitudes are catalogue values; a planet's comes from the same distances as its position (Schlyter's formulas, Saturn's rings included), the Moon's from its phase, and the Sun's is $-26.7$.

Show All with the crosshair on the Moon

Objects are listed alphabetically in the Planets and Stars menus. The catalogue keeps its own order (stars by brightness, planets by distance out from the Sun), and a star id is its position in it, so only the menu presentation is sorted.

The grids

Horizon grid at 10 degrees, near the zenith

Aimed near the zenith with the horizon grid at $10^\circ$, the finest spacing, where the vertical circles converge on the point overhead.

Settings

Hold the up/menu button on any sky screen. Every setting changes what is on the screen behind, so they are reachable from the screen they affect rather than only from the root menu. Selecting an item steps it to its next value in place. A short list is quicker to thumb through than a submenu, and the label cannot go stale behind the menu showing it.

Settings

Setting Values Default
Horizon Grid Off · 60 · 45 · 30 · 15 · 10 degrees Off
Equatorial Grid Off · 60 · 45 · 30 · 15 · 10 degrees Off
Constellations Off · On Off
Sun Path Off · On Off
Moon Path Off · On Off
Crosshair Off · On Off
Update Location One fix only · every 5 / 15 / 30 / 60 min One fix only

Watches that cannot draw the finest spacings in time, or hold them in memory, stop at 15 or 30 degrees.

Spacings that come to a whole number of hours say so. The sky turns $360^\circ$ in 24 hours, so $15^\circ$ is one hour of it and the grid divides the sky into hour-wide cells.

Everything starts off; the grids are there when you ask for them. The equatorial grid turns with the sky, as the stars on it do, and the horizon grid never moves.

The display is held awake while a sky screen is up. When burn-in protection refuses to keep it on, the panel gets a short rest before the app asks again.


The maths

Everything below runs on the watch from the clock and two sensors, with no almanac file and no network connection.

1. Time

Julian Day

SkyMath.julianDay uses the standard Gregorian algorithm. For year $y$ and month $m$, with January and February counted as months 13 and 14 of the previous year:

$$a = \left\lfloor \frac{y}{100} \right\rfloor \qquad b = 2 - a + \left\lfloor \frac{a}{4} \right\rfloor$$ $$JD = \big\lfloor 365.25\,(y + 4716) \big\rfloor + \big\lfloor 30.6001\,(m + 1) \big\rfloor + d + b - 1524.5 + \frac{h + \dfrac{\mathit{min}}{60} + \dfrac{s}{3600}}{24}$$

This one must be 64-bit. A Julian Day this century runs to about $2.46 \times 10^{6}$, and a 32-bit float carries roughly seven significant digits. That is enough for the date but not for the time of day, which rounds to the nearest six hours.

Measured against the sky: an observation at 04:06 wants $JD = 2461286.37917$; a float holds $2461286.5$, and those 2.9 hours moved Saturn from $61^\circ$ up in the south-west down to $19^\circ$. Whole evenings collapse onto one stored value, so a position sits frozen and then jumps. The leading .toDouble() forces the running total to 64 bits; the terms before it are exact whole numbers that a float still holds safely.

Sidereal time

Greenwich Mean Sidereal Time in degrees, with $D$ the days since J2000 and $T = D / 36525$:

$$\mathrm{GMST} = 280.46061837 + 360.98564736629\,D + 0.000387933\,T^{2} - \frac{T^{3}}{38710000}$$ $$\mathrm{LST} = \mathrm{GMST} + \lambda_{\text{obs}} \qquad (\lambda_{\text{obs}} \text{ east-positive})$$

Also 64-bit, and for the same reason: $D$ is multiplied by 361 before being wrapped back into a circle, passing through three and a half million degrees on the way.

The $0.98564736629$ is there because the sky turns slightly more than $360^\circ$ per solar day, which is why a star rises about four minutes earlier each night.

2. Where the object is

Every object resolves to a geocentric right ascension $\alpha$ and declination $\delta$ at a given Julian Day. Four different routes get there:

flowchart TD
    OBJ["Object from the catalogue"] --> T{"type"}
    T -->|star| S["Read J2000 RA and Dec<br/>straight from the table"]
    T -->|sun| SU["Meeus abbreviated<br/>mean longitude, anomaly,<br/>equation of the centre"]
    T -->|moon| MO["Meeus series<br/>largest terms only"]
    T -->|planet| PL["Keplerian elements<br/>Newton solve for E"]
    PL --> HG["Heliocentric to geocentric<br/>add the Sun vector"]
    SU --> EQ["Ecliptic to equatorial<br/>rotate by the obliquity"]
    MO --> EQ
    HG --> EQ
    EQ --> OUT["RA and Dec"]
    S --> OUT
Loading

Stars (SkyCatalog)

Bright naked-eye stars as literal J2000 RA/Dec plus visual magnitude. Proper motion and precession are ignored; both are far below what a wrist magnetometer can resolve.

Sun (SolarLunar.sunPosition)

Meeus, abbreviated. Mean longitude, mean anomaly, equation of the centre, then the apparent longitude corrected for aberration and nutation:

$$\begin{aligned} L_0 &= 280.46646 + 36000.76983\,T + 0.0003032\,T^{2} \\[2pt] M &= 357.52911 + 35999.05029\,T - 0.0001537\,T^{2} \\[2pt] C &= \left(1.914602 - 0.004817\,T - 0.000014\,T^{2}\right)\sin M \\\ &\quad + \left(0.019993 - 0.000101\,T\right)\sin 2M + 0.000289 \sin 3M \\[2pt] \Omega &= 125.04 - 1934.136\,T \\[2pt] \lambda &= L_0 + C - 0.00569 - 0.00478 \sin\Omega \\[2pt] \varepsilon &= \varepsilon_0 + 0.00256 \cos\Omega \end{aligned}$$

Moon (SolarLunar.moonPosition)

The Moon needs a series rather than a formula. It is built from four fundamental arguments (mean elongation $D$, solar anomaly $M$, lunar anomaly $M'$ and argument of latitude $F$), with truncated longitude and latitude series. The largest terms:

$$\begin{aligned} \Delta\lambda &= 6.288774 \sin M' + 1.274027 \sin(2D - M') + 0.658314 \sin 2D + \cdots \\[2pt] \Delta\beta &= 5.128122 \sin F + 0.280602 \sin(M' + F) + 0.277693 \sin(M' - F) + \cdots \end{aligned}$$

$6.29 \sin M'$ is the elliptical orbit; $1.27 \sin(2D - M')$ is evection, the Sun stretching the orbit; $0.66 \sin 2D$ is variation. $5.13 \sin F$ is the $5.1^\circ$ tilt of the Moon's orbit against the ecliptic.

Planets (Planets)

Mercury through Saturn from approximate Keplerian elements, in the style of Paul Schlyter's How to compute planetary positions. Each planet's elements (ascending node $N$, inclination $i$, argument of perihelion $w$, semi-major axis $a$, eccentricity $e$, mean anomaly $M$) are linear in the day number.

Kepler's equation has no closed form:

$$M = E - e \sin E$$

so it is solved by Newton iteration from a first guess, to $10^{-6}$ degrees or eight passes:

$$E_0 = M + \frac{180}{\pi}\,e \sin M \,\left(1 + e \cos M\right)$$ $$E_{n+1} = E_n + \frac{M - \left(E_n - \dfrac{180}{\pi} e \sin E_n\right)} {1 - e \cos E_n}$$

Then true anomaly and radius from the eccentric anomaly, rotated out of the orbital plane into heliocentric ecliptic coordinates, and the Sun's own geocentric vector added to shift the origin from the Sun to the Earth:

$$x_g = x_h + x_\odot \qquad y_g = y_h + y_\odot \qquad z_g = z_h$$

That last addition is the parallax of the whole Earth's orbit, and it makes Mars swing backwards through the sky a few weeks a year.

Ecliptic to equatorial

All three computed bodies come out in ecliptic coordinates and are rotated by the obliquity $\varepsilon = 23.439291 - 0.0130042,T - \cdots$:

$$\sin\delta = \sin\beta \cos\varepsilon + \cos\beta \sin\varepsilon \sin\lambda$$ $$\tan\alpha = \frac{\sin\lambda \cos\varepsilon - \tan\beta \sin\varepsilon} {\cos\lambda}$$

3. Turning that into a direction in the sky

SkyMath.raDecToAltAz. With hour angle $H = \mathrm{LST} - \alpha$ and observer latitude $\varphi$:

$$\sin h = \sin\delta \sin\varphi + \cos\delta \cos\varphi \cos H$$ $$\cos A = \frac{\sin\delta - \sin\varphi \sin h}{\cos\varphi \cos h}$$ $$A_{\text{az}} = \begin{cases} 360^\circ - A, & \sin H > 0 \\\ A, & \text{otherwise} \end{cases}$$

Azimuth is measured from north, clockwise through east. The $\sin H$ branch resolves the ambiguity that $\arccos$ leaves: whether the object is east or west of the meridian.

Then to a world East-North-Up unit vector:

$$\mathbf{t} = \big(\, \cos h \sin A_{\text{az}},\; \cos h \cos A_{\text{az}},\; \sin h \,\big)$$

The equatorial grid, the constellation lines and the Sun and Moon paths skip those two steps. SkyMath.equatorialToEnu writes the same rotation as one matrix, without the arcsine, arccosine and quadrant test, and gives the same answer to within floating-point noise:

$$\begin{aligned} E &= -\cos\delta \sin H \\\ N &= \sin\delta \cos\varphi - \cos\delta \cos H \sin\varphi \\\ U &= \cos\delta \cos H \cos\varphi + \sin\delta \sin\varphi \end{aligned}$$

Those overlays keep their points as fixed unit vectors (SkyMath.raDecToVector), so each frame turns them with nine multiplications apiece.

4. Corrections

Both act along the vertical circle, so azimuth is untouched.

Parallax

Everything above answers for the centre of the Earth. You are 6378 km off that centre. For the Moon that matters:

$$h' = h - \pi_{\!h} \cos h$$

where $\pi_{!h}$ is the horizontal parallax from Meeus' series:

$$\pi_{\!h} = 0.9508 + 0.0518 \cos M' + 0.0095 \cos(2D - M') + 0.0078 \cos 2D + 0.0028 \cos 2M'$$

That runs to about $0.95^\circ$, nearly two full-Moon widths, which is why the Moon is the only body corrected. The Sun comes to $0.0024^\circ$, the planets at closest approach to $0.009^\circ$, and the stars to nothing. Earth's polar flattening would move the answer by about 12 arcseconds and is ignored.

Refraction

Bennett's formula, giving arcminutes for an altitude $h$ in degrees:

$$R = \frac{1}{\tan\!\left(h + \dfrac{7.31}{h + 4.4}\right)}$$

About $0.57^\circ$ at the horizon, $0.09^\circ$ at ten degrees up, and negligible overhead, so it matters most for objects just clearing the skyline, which are the hardest to find anyway. Refraction is also why the Sun is still visible after it has geometrically set.

5. Where the watch is pointing

Two sensors, polled at 10 Hz rather than waited on. The push callbacks fire about once a second, which is far too slow and too stale to aim with while moving.

Smoothing

Exponential smoothing applied to the raw sensor vectors, not to angles derived from them:

$$\mathbf{v}_n = \mathbf{v}_{n-1} + \alpha\left(\mathbf{s}_n - \mathbf{v}_{n-1}\right), \qquad \alpha = 0.2$$

Stellarium's SensorsMgr does the same, for the same reason: magnetometer noise is what makes a sky view jitter, and it settles far better when averaged as vectors. Stellarium runs $\alpha = 0.01$ to $0.1$ per frame at display rate and smooths harder the tighter the field of view; $0.2$ at 10 Hz is the equivalent for an $8^\circ$ lock.

Heading is smoothed the short way round the circle, so $359^\circ \to 1^\circ$ does not swing backwards through $358^\circ$.

Gravity

$$\hat{\mathbf{u}} = -\,\frac{\mathbf{a}}{\lVert \mathbf{a} \rVert}$$

The sign comes from the hardware: this device reports the gravity direction rather than specific force. Each such convention lives in a named constant with a check you can run on the pointer screen. Aim the back at the horizon and the aim elevation should read about $0^\circ$; tip it skyward and it should climb.

Tilt-compensated magnetic azimuth

The system's compass heading is a flat reading and goes wrong as soon as you tilt the watch up at the sky, which is how this app is used. So the azimuth is worked out from the raw magnetometer instead.

Flatten both the field $\mathbf{m}$ and the back axis $\mathbf{b}$ onto the plane at right angles to gravity:

$$\hat{\mathbf{n}} = \frac{\mathbf{m} - (\mathbf{m}\cdot\hat{\mathbf{u}})\,\hat{\mathbf{u}}} {\lVert \cdots \rVert} \qquad \hat{\mathbf{p}} = \frac{\mathbf{b} - (\mathbf{b}\cdot\hat{\mathbf{u}})\,\hat{\mathbf{u}}} {\lVert \cdots \rVert}$$ $$A_{\text{mag}} = \mathrm{atan2}\!\big( (\hat{\mathbf{p}} \times \hat{\mathbf{n}})\cdot\hat{\mathbf{u}},\; \hat{\mathbf{p}} \cdot \hat{\mathbf{n}} \big)$$

With the vertical component of the field removed, what is left points north however the watch is tilted. Stellarium does the same, de-rotating the raw field by the device's own roll and pitch rather than trusting a system heading.

Magnetic declination

The system heading is still useful in one case: while the watch happens to be near level, it is both trustworthy and corrected to true north. The gap between it and the computed magnetic azimuth is therefore the local declination:

$$\delta_{\text{mag}} \leftarrow \delta_{\text{mag}} + 0.05\left(\theta_{\text{system}} - A_{\text{mag}}\right)$$

banked slowly, only while the watch is near level, and then applied at any tilt. Averaging stops once it has converged, so the reference stops creeping. It is worked out afresh each time a sky screen opens.

The device frame

DeviceAim.deviceFrame returns the world's axes written in the watch's coordinates:

$$\hat{\mathbf{e}} = \hat{\mathbf{n}} \times \hat{\mathbf{u}} \qquad \hat{\mathbf{n}},\ \hat{\mathbf{u}} \text{ as above}$$

and is then swung round to true north by the banked declination $\delta_{\text{mag}}$, which keeps the picture and the readouts in the same frame. Every object on this screen came out of RA/Dec and sidereal time, so it is true-referenced by construction; the magnetometer is not. Declination is measured east-positive, so magnetic north sits that far east of true and true north is the same angle back the other way:

$$\hat{\mathbf{n}}_{\text{true}} = \hat{\mathbf{n}}\cos\delta_{\text{mag}} - \hat{\mathbf{e}}\sin\delta_{\text{mag}} \qquad \hat{\mathbf{e}}_{\text{true}} = \hat{\mathbf{e}}\cos\delta_{\text{mag}} + \hat{\mathbf{n}}\sin\delta_{\text{mag}}$$

Without this rotation the whole drawn sky would sit turned by the local declination against the numbers printed beneath it: a few degrees in most places and twenty in parts of Canada, against an $8^\circ$ lock.

viewOffset then takes a world direction $\mathbf{t} = t_E\hat{\mathbf{e}} + t_N\hat{\mathbf{n}} + t_U\hat{\mathbf{u}}$ and reads off its components along the watch's own axes ($\hat{\mathbf{x}}$ at 3 o'clock, $\hat{\mathbf{y}}$ at 12 o'clock, $\hat{\mathbf{z}}$ out through the screen):

$$\begin{aligned} \mathit{right} &= \mathbf{t} \cdot \hat{\mathbf{x}} \\[2pt] \mathit{up} &= \mathbf{t} \cdot \hat{\mathbf{y}} \\[2pt] \mathit{forward} &= -\,\mathbf{t} \cdot \hat{\mathbf{z}} \end{aligned}$$

Forward is the component whose sign flips, because the aim is out through the back. All three are direction cosines, ready for a perspective divide.

6. Putting the sky on the screen

A pinhole camera, with $f$ the focal length in pixels:

$$x = c_x + f\,\frac{\mathit{right}}{\mathit{forward}} \qquad y = c_y - f\,\frac{\mathit{up}}{\mathit{forward}}$$ $$f = \frac{c_x}{\tan 45^\circ} = c_x \quad\Longrightarrow\quad \mathrm{FOV} = 2\arctan\!\left(\frac{c_x}{f}\right) = 90^\circ$$

so the screen spans $90^\circ$ of sky across its width, and the same down its height on a square display. The perspective divide is what makes the sky line up the way a camera would, rather than merely pointing in the right general direction: at the edges of a $90^\circ$ field the difference between the two is large.

Anything with $\mathit{forward} &lt; 0.1$ (more than about $84^\circ$ off the aim) is level with the back of the watch or behind it, where a perspective projection has nothing to say. It is dropped, which breaks grid lines cleanly instead of folding them back across the view.

Nothing is clipped and no band is reserved. The sky is laid down across the whole display and the text is drawn on top of it afterwards.

7. The sky overlays

Five things can be drawn over the sky, each switched on separately: two grids that answer different questions, the constellation figures, and the paths of the Sun and the Moon.

anchored to moves when
Horizon (HorizonGrid) — circles of equal altitude, vertical circles between zenith and nadir the ground and the compass you move the watch
Equatorial (EquatorialGrid) — circles of equal declination, hour circles between the celestial poles the stars you move the watch, and as time passes
Constellations (Constellations) — stick figures joining their stars the stars you move the watch, and as time passes
Sun and Moon paths (SkyPaths) — where each will be, marked by date the stars you move the watch, and as time passes
flowchart LR
    subgraph H["Horizon grid"]
        direction LR
        HA["az, alt"] --> HB["ENU vector"]
    end
    subgraph E["Equatorial grid"]
        direction LR
        EA["Horizon grid mesh<br/>east and north swapped"] --> EB["equatorialToEnu<br/>one matrix a frame"]
        LATLST["latitude and<br/>sidereal time"] --> EB
        EB --> EC["ENU vector"]
    end
    HB --> P["Rotate into watch axes,<br/>perspective divide"]
    EC --> P
    P --> SCR["Screen"]
Loading

The horizon grid takes no clock and no position: $(A_{\text{az}}, h)$ maps straight to a direction. Rest the watch flat on a table and it looks at the nadir, with the vertical circles converging in the middle of the screen.

The equatorial grid has no points of its own. A horizon-grid point with its east and north parts swapped is the equatorial unit vector at the same two numbers, so it draws the horizon grid's mesh through $\text{equatorialToEnu}(\varphi, \mathrm{LST})$ with those two columns swapped, and both grids share one mesh when their spacing matches. Sidereal time is the only thing in that chain that moves, so the grid turns with the sky at $15^\circ$ per hour, one full revolution per sidereal day, pivoting about the celestial poles at altitude $= \varphi$, and the stars keep their places on it. Turn both grids on and you can watch the blue grid slide past the stationary green one.

Both count outwards from their zero line (the horizon for one, the celestial equator for the other) rather than up from the bottom, so that line is drawn at every spacing.

Cardinal letters are drawn even with the horizon grid off, since which way you are facing is the most directly useful thing on the screen.

Constellations

Orion, Ursa Major, Ursa Minor, Cassiopeia, Cygnus, Crux and the twelve constellations of the zodiac, drawn as lines joining their stars, with a dot on each star. The watches with half the per-frame budget carry Leo and Scorpius but not the other ten zodiac figures: a frame with all of them on top of everything else is more than those watches allow.

The vertices live in Constellations rather than in SkyCatalog, because most of them are not stars anyone would aim at. A figure needs its faint stars to read, and adding them to the catalogue would bury the bright ones in the Stars menu and scatter faint dots across Show All. They are line endpoints, not objects, so each figure is held as runs of RA/Dec pairs, one per unbroken stroke.

One thing separates them from the equatorial grid they share a frame with. Like the Sun and Moon paths, they apply refraction, because they have to stay under the stars they join: the stars are placed through apparentAltitude, so a belt drawn without it sits a couple of pixels below its own three stars as the constellation rises.

Sun and Moon paths

The Sun's path through the next twelve months, with a dot on the first of each month, and the Moon's through the next four weeks, with a dot and the date on each day. Both come from the same series that place the bodies, with the same parallax and refraction, so the Sun and the Moon each sit on their own line. Each line has its own switch.

The dated positions are fixed on the sky, so they are worked out once a month for the Sun and once a day for the Moon, from the view timer a few days at a time, and placed for your time and position every few seconds.

8. Turn-and-tilt guidance

Measured against gravity rather than the watch's own axes, so rolling your wrist leaves it alone. aimBasis builds a frame from the aim direction (heading $\theta$, elevation $\epsilon$) plus a horizontal "right" and a perpendicular "up":

$$\mathbf{d} = \big( \cos\epsilon \sin\theta,\; \cos\epsilon \cos\theta,\; \sin\epsilon \big)$$ $$\hat{\mathbf{r}} = \frac{\mathbf{d} \times \hat{\mathbf{z}}}{\cos\epsilon} = \frac{(d_N,\; -d_E,\; 0)}{\cos\epsilon} \qquad \hat{\mathbf{u}}_a = \hat{\mathbf{r}} \times \mathbf{d}$$

and project puts the object direction $\mathbf{t}$ into it:

$$\mathit{turn} = \mathrm{atan2}\!\big( \mathbf{t}\cdot\hat{\mathbf{r}},\; \mathbf{t}\cdot\mathbf{d} \big)$$ $$\mathit{tilt} = \mathrm{atan2}\!\left( \mathbf{t}\cdot\hat{\mathbf{u}}_a,\; \sqrt{(\mathbf{t}\cdot\mathbf{d})^{2} + (\mathbf{t}\cdot\hat{\mathbf{r}})^{2}} \right)$$

Tilt is measured off the aim's own horizontal plane, not straight off the aim axis. Both amount to the same thing near the object, but this stays well defined when it is far to one side, where "aim" and "up" both fall to zero and dividing one by the other is meaningless.

Aimed within a couple of degrees of straight up or down there is no sensible "turn left" to give, since every direction is sideways from there, so the screen says Straight up/down instead; the picture still holds.

9. Drawing the objects

ObjectArt. Stellarium wraps photographic surface maps onto spheres on the GPU. None of that survives the trip down to a disc a few dozen pixels across, so what is drawn is the handful of features still recognisable at that size.

Moon phase

The phase is computed from the same geometry Stellarium uses. With $\mathbf{m}$ the direction to the Moon and $\mathbf{s}$ the direction to the Sun, both unit vectors, their dot product is the cosine of the elongation and the lit fraction falls straight out:

$$k = \frac{1 - \mathbf{m}\cdot\mathbf{s}}{2}$$

What the drawing wants is the terminator ellipse's semi-axis as a fraction of the disc radius, $c = 2k - 1$, which reduces to:

$$c = -\left(\mathbf{m}\cdot\mathbf{s}\right)$$

running from $-1$ at new, through $0$ at half, to $+1$ at full. The bright limb faces the Sun, so the whole figure is oriented by the tangent direction from the Moon towards it:

$$\mathbf{t} = \mathbf{s} - (\mathbf{s}\cdot\mathbf{m})\,\mathbf{m}$$

taken from the geometry rather than off the screen, which is why it stays right as your wrist rolls.

It is drawn as a dark disc, then the bright hemisphere, then the terminator ellipse in whichever colour that side ended up. That makes two convex half-ellipses rather than one lune, because a crescent is concave and how a device fills a concave polygon is not something to rely on.

flowchart LR
    A["Dark disc<br/>full radius"] --> B["Bright hemisphere<br/>half-ellipse, semi-axis r"]
    B --> C{"c greater than<br/>or equal to 0?"}
    C -->|"yes, gibbous"| D["Half-ellipse, semi-axis -c·r<br/>painted light"]
    C -->|"no, crescent"| E["Half-ellipse, semi-axis -c·r<br/>painted dark"]
Loading

The rest

  • Sun — corona rings outside the disc, and a disc brightening towards the middle for limb darkening.
  • Saturn — rings, drawn before the disc so the planet sits in front of them.
  • Jupiter — two belts either side of the equator, which is as much as any small telescope shows.
  • Stars — magnitude-scaled radius $r = \mathrm{clamp}\left(5 - \mathit{mag},, 2,, 7\right)$, so brighter stars draw larger the way they look.

Rings and belts lie along the local horizontal, derived from where the zenith falls in the watch axes, so they roll with the sky rather than staying pinned to the screen.

Source map

File What it does
PointerView.mc The sky screen: sensors, layout, both modes, drawing
ObjectArt.mc Colour, size and face of each body; Moon phase
DeviceAim.mc Orientation, tilt-compensated compass, projection, overlay lines
Settings.mc Persisted choices, cached in memory
SkyMath.mc Time, coordinate conversion, refraction, parallax
HorizonGrid.mc Alt/az grid, the mesh both grids share, and cardinal letters
EquatorialGrid.mc RA/Dec grid, drawn from the horizon grid's mesh
Constellations.mc Stick figures, drawn from their own vertices
SkyPaths.mc Sun and Moon calendar lines
Planets.mc Keplerian planetary positions
SolarLunar.mc Sun and Moon series, obliquity, parallax
SkyCatalog.mc The object registry, the bright-star table, RA/Dec dispatch
SettingsMenuDelegate.mc Cycling settings in place
SkyMenus.mc Menu construction
RootMenuDelegate.mc Root and object-list menu routing
PointerDelegate.mc Back, and the menu button
MiniverseApp.mc Entry point
test/ Unit tests

Cost and frame time

onUpdate runs at 10 Hz, and what dominates the frame is grid density and catalogue size.

  • Grids: the point count grows as the spacing shrinks. Both grids keep fixed points, built once per spacing from the view timer rather than in the draw, and each frame only rotates them. Watches with less memory or less time per frame stop at a coarser finest spacing.
  • Show All works out where every object is every few seconds and holds the result, since the sky moves well under a pixel in that time. Only the projection is redone per frame, and the cache is dropped when your position changes.
  • Position is event-driven, not per-frame. A cached fix is used immediately; if nothing usable arrives within a few seconds the GPS is driven actively.

Building

Requires the Connect IQ SDK and a developer key.

monkeyc -f monkey.jungle -o bin/Miniverse.prg -y developer_key -d instinctcrossoveramoled

Run it in the simulator with connectiq followed by monkeydo bin/Miniverse.prg instinctcrossoveramoled, or build for the store with -e -r. For the unit tests, add -t to the build and /t to monkeydo.

Permissions: Positioning and Sensor.

Devices

Every product listed in manifest.xml, across round, rectangular and semi-octagonal displays. Requires Connect IQ API level 3.4 or later.

The black-and-white watches (the Instinct 2 family, the Instinct 3 Solar, the Instinct E and the Descent G1) show dark shades, red, green and orange as black, so on those everything that is not black is drawn white. On the watches with a small round window in the top right corner, the object name and the grid labels are kept clear of it. On the smallest screens the readout steps down a size, and drops its column heads if it still will not fit, so the object stays clear of the table describing it, and two grid labels share a line.

Show All on the Instinct 2, drawn in white Layout is derived from the display size and the real font metrics rather than fixed pixel offsets, so the picture takes every row the text does not need. On round displays, labels are pulled in to where the glass reaches on each row.

A watch without a magnetometer shows No compass on this watch rather than waiting for a reading that never comes.

Accuracy

Accuracy is limited by the magnetometer. A wrist compass resolves a few degrees at best, and every approximation here is chosen to sit comfortably underneath that:

Source Error
Sun position $&lt; 0.01^\circ$
Moon position $\approx 0.02^\circ$
Planets arcminutes
Stars (no proper motion or precession) arcminutes
Refraction (Bennett) $&lt; 0.02^\circ$ above $5^\circ$ altitude
Earth's flattening, ignored $\approx 12''$
Wrist magnetometer a few degrees

Licence

GPL-3.0. See LICENSE.

Three parts of the approach follow Stellarium: smoothing raw vectors rather than angles, de-rotating the raw field instead of trusting a system heading, and taking the lunar phase from the elongation. No Stellarium code or assets are used. Solar and lunar series are from Jean Meeus, Astronomical Algorithms; the planetary elements follow Paul Schlyter's How to compute planetary positions. The zodiac line figures follow those published with d3-celestial (BSD 3-Clause).

Releases

Packages

Used by

Contributors

Languages