Appearance
Sun and moon
Two topics, sextant/sun and sextant/moon, carry everything the boat knows about the two bodies that matter to a watch schedule: when it gets light, when it gets dark, how much moon there will be, and where either of them is right now.
For the crew this is the sunrise/sunset panel and the moon phase icon. The one thing worth knowing before reading further: the three twilight times are not decoration. Nautical twilight is the window in which the horizon is still visible at sea and a sextant sight is possible; astronomical twilight is when the sky is properly dark. A passage plan that wants "arrive before dark" usually means civil twilight end, not sunset.
Both reports are produced by the same module, from the same recompute trigger, and are published together. Both are retained with expireAfterSec: 3600.
Twilight, in one table
| Twilight | Sun below the horizon | What it looks like |
|---|---|---|
| Civil | 0° to 6° | Outdoor activity without lights. Horizon clearly visible. |
| Nautical | 6° to 12° | Horizon still faintly visible at sea — the window for celestial sights. |
| Astronomical | 12° to 18° | Dark enough for most observation; a faint glow remains. Beyond 18° is full night. |
Each has a start, in the morning before sunrise, and an end, in the evening after sunset. So the full sequence on an ordinary day, in order, is:
astronomicalTwilightStart
nauticalTwilightStart
civilTwilightStart
rise
transit
set
civilTwilightEnd
nauticalTwilightEnd
astronomicalTwilightEndEvery instant is published twice
Every time in these two payloads appears as both an ISO-8601 UTC string and a Unix-epoch number under a …Timestamp sibling key:
json
"rise": "2026-08-16T04:52:33Z",
"riseTimestamp": 1786935153,The two always name the same instant to the second — the number is derived from the same broken-down UTC fields the string is formatted from, truncation included — so a consumer can schedule off the number without re-parsing the string.
They are also omitted together. When an event does not occur, both the string and its timestamp are absent from the payload rather than being null or a sentinel value.
When events do not occur
Every time in the two reports is the next occurrence of its event after the instant of the report. Above the Arctic Circle and below the Antarctic Circle the sun does not always rise or set, and the twilight bounds can be missing independently of the rise and set. Above about 61° of latitude the moon can circle the pole or stay down for days.
The daemon does not invent a value in those cases. A field is left out, with its …Timestamp and its bearing or altitude, when its event does not come:
| Absent | Meaning |
|---|---|
sun: rise, riseAzimuth, set, setAzimuth, transit, transitAltitude, all together | the sun does not rise or does not set that day at this latitude |
| sun: a twilight bound | the sun does not pass 6°, 12° or 18° below the horizon, in that direction, within the next 48 hours |
moon: rise, riseAzimuth | no moonrise within the next 48 hours |
moon: set, setAzimuth | no moonset within the next 48 hours |
moon: transit, transitAltitude | no culmination above the horizon within the next 48 hours |
The moon's three events are present or absent each on its own: a moon circling the pole has a transit and neither rise nor set. Below about 61° of latitude all three are always there.
A midsummer payload from northern Norway can therefore carry an altitude and an azimuth and nothing else. That is correct, and a consumer must treat every time field as optional.
altitude and azimuth are always present. They are the body's position at the moment of the recompute.
The sun report — sextant/sun
| Field | Unit | Notes |
|---|---|---|
rise / riseTimestamp | ISO-8601 UTC / epoch seconds | optional |
riseAzimuth | degrees, 0 = North | compass bearing of the rising point; optional |
set / setTimestamp | ISO-8601 UTC / epoch seconds | optional |
setAzimuth | degrees, 0 = North | optional |
transit / transitTimestamp | ISO-8601 UTC / epoch seconds | local solar noon; optional |
transitAltitude | degrees | the sun's maximum altitude for the day; optional |
civilTwilightStart / …End (+ …Timestamp) | ISO-8601 UTC / epoch | optional |
nauticalTwilightStart / …End (+ …Timestamp) | ISO-8601 UTC / epoch | optional |
astronomicalTwilightStart / …End (+ …Timestamp) | ISO-8601 UTC / epoch | optional |
altitude | degrees | negative below the horizon; always present |
azimuth | degrees, 0 = North, 90 = East | always present |
transitAltitude is a useful number in its own right: it is the sun's altitude at meridian passage, which is what a noon sight measures.
All azimuths are compass bearings — 0° North, 90° East. The underlying library measures azimuth from South; the daemon converts.
The moon report — sextant/moon
| Field | Unit | Notes |
|---|---|---|
rise, set, transit (+ …Timestamp) | ISO-8601 UTC / epoch | the next moonrise, moonset and culmination, below; optional |
riseAzimuth, setAzimuth | degrees, 0 = North | the moon's bearing at rise and at set; optional |
transitAltitude | degrees | the moon's altitude at transit; optional |
altitude, azimuth | degrees | always present |
phase | 0.0 – 1.0 | position in the synodic cycle; always present |
illumination | 0.0 – 1.0 | fraction of the disc lit; always present |
phaseName | identifier | one of eight names, below; always present |
waxing | boolean | true from new moon to full moon; always present |
age | days | time since the last new moon; always present |
litSide | right or left | the side of the disc that is lit, by the hemisphere's convention; optional |
litAngle | degrees, 0 – 360 | where the lit side really is, clockwise from the vertical; always present |
track | list of points | the moon's path across the sky for one pass, below; optional |
daysToNewMoon | days | optional |
daysToFullMoon | days | optional |
rise, set and transit: the next of each
rise is the next moonrise after the instant of the report, set the next moonset, and transit the next culmination: the moon's passage across the meridian, above the horizon. Each is looked for on its own, so the three do not always belong to one pass. With the moon down they are the coming pass, in order. With the moon up, set is the end of the pass in progress, transit is its culmination until the moon has crossed the meridian, and rise is the moonrise after that.
- None is ever behind the report. Once an event has passed, its field names the following one, about a day later.
- An event keeps its instant from one report to the next, to the second, for as long as it is ahead. Each instant is a whole second: the first one after the event.
- Rise and set are crossings of the moon's computed altitude: the instants its centre passes 0.125°, the altitude at which the library takes the moon to rise and to set.
transitis the instant its hour angle passes zero.riseAzimuth,setAzimuthandtransitAltitudeare the moon's position at those three instants.
Off Belle-Île on 31 August 2026 the moon rises at 19:54:15 UTC. A report at 19:53 gives that instant as rise; a report at 19:55 gives the next moonrise, 20:15:54 on 1 September, and both give the same transit, 03:04:22, and the same set, 10:31:52.
phase and illumination are different things
This trips up every consumer at least once.
illuminationis the fraction of the moon's disc that is lit. It runs 0 at new moon up to 1 at full moon and back down, so it takes every value twice per lunation and cannot tell you whether the moon is waxing or waning.phaseis the position in the synodic cycle, and it is monotonic:phaseMoon 0.0 new 0.25 first quarter 0.5 full 0.75 last quarter 1.0 next new A screen needs neither number to name the moon or to know which way round to draw it:
phaseName,waxing,age,litSideandlitAngle, below, say it outright. Size the lit part fromillumination.
phase is computed from the Moon–Sun elongation in ecliptic longitude, which is the textbook definition of phase and age. It is deliberately not the Sun–Moon–Earth angle that libnova's ln_get_lunar_phase() returns — that angle runs from 0° at full to 180° at new and back, and has the same ambiguity as illumination.
The moon, ready to display: phaseName, waxing, age, litSide, litAngle
Five fields carry what a screen shows. They are worked out once, in the daemon, so every screen on board names and draws the same moon; a consumer displays them and derives nothing.
phaseName is one of eight identifiers, read from phase. The elongation is the angle between the moon and the sun along the ecliptic, phase × 360°:
phaseName | Elongation | phase | Lit fraction | Lasts about |
|---|---|---|---|---|
new | 340° through 0° to 20° | 0.944 through 0.0 to 0.056 | under 3 % | 3.3 days |
waxing-crescent | 20° to 70° | 0.056 to 0.194 | 3 % to 33 % | 4.1 days |
first-quarter | 70° to 110° | 0.194 to 0.306 | 33 % to 67 % | 3.3 days |
waxing-gibbous | 110° to 160° | 0.306 to 0.444 | 67 % to 97 % | 4.1 days |
full | 160° to 200° | 0.444 to 0.556 | over 97 % | 3.3 days |
waning-gibbous | 200° to 250° | 0.556 to 0.694 | 97 % to 67 % | 4.1 days |
last-quarter | 250° to 290° | 0.694 to 0.806 | 67 % to 33 % | 3.3 days |
waning-crescent | 290° to 340° | 0.806 to 0.944 | 33 % to 3 % | 4.1 days |
Each band includes its lower bound. A principal phase keeps its name within 20° of elongation either side of its exact position — 20/360 of the cycle, about 1.64 days. The window is that wide because the name describes how the moon looks, not the instant of the event: within 20° of full the disc is more than 97 % lit, which is what a crew sees as a full moon. For the event itself, read daysToNewMoon and daysToFullMoon.
The quarters are as wide as new and full: first-quarter and last-quarter name the moon from 33 % to 67 % lit, not only on the day it is half lit.
The durations are averages. The moon's speed varies along its orbit, so a principal name lasts between 2.8 and 3.7 days and a crescent or gibbous one between 3.6 and 4.6.
They are identifiers, not display text. An interface translates them.
waxing is true from new moon to full moon and false from full moon to new moon; it is exactly phase < 0.5. Inside the new and full bands it changes at the instant of the event, so full with waxing: true is the day and a half before the full moon and full with waxing: false the day and a half after it.
age is the number of days since the last new moon, counted from the instant of that new moon. It is not phase × 29.53: the moon's speed varies along its orbit, a lunation lasts anywhere from 29.3 to 29.8 days, and that product is off by up to 0.9 day. The daemon finds the instant of the last new moon to within about two minutes of an almanac, so age is good to 0.002 day. It returns to 0 at each new moon.
litSide is right or left: the side of the disc that is lit, by the convention of the hemisphere the boat is in — the way a calendar printed for that hemisphere draws the moon. It follows the boat's latitude, the one the report was computed for:
| Boat's latitude | Waxing | Waning |
|---|---|---|
| north of the equator | right | left |
| south of the equator | left | right |
Through the new and full bands it keeps following waxing and changes side at the instant of the event, when the disc is all dark or all lit and the side makes no difference to the picture. A drawing therefore needs no special case: put the lit part on litSide and size it from illumination.
litSide is absent when the latitude is exactly 0: on the equator there is no hemisphere to take the side from, and the daemon does not guess one.
phaseName, waxing and litSide are three readings of the same phase, by one rule, so they never disagree: first-quarter and the two waxing-… names always come with waxing: true, last-quarter and the two waning-… names with waxing: false, and litSide is the side the table above gives for that waxing.
litAngle is where the lit side really is, as the crew sees the moon at that moment. It is the direction of the middle of the lit limb, in degrees, measured from the vertical — the line from the centre of the disc towards the zenith — and counted clockwise as the observer looks at the moon, as on a screen that shows the sky with up at the top:
litAngle | The lit side is |
|---|---|
| 0 | straight up |
| 90 | on the right |
| 180 | straight down: a crescent lying on its back |
| 270 | on the left |
It runs from 0, included, to 360, excluded. To draw the moon as it stands in the sky, draw it lit on the right and turn the drawing clockwise by litAngle − 90.
The lit limb always faces the sun, so litAngle is the direction of the sun from the moon, seen from the boat. It changes with the hour, as the moon crosses the sky, and with the latitude. On the evening of 16 August 2026, at dusk, the moon is a waxing crescent, 20 % lit:
| Boat | Instant (UTC) | Moon's altitude | litAngle | The crew sees |
|---|---|---|---|---|
| off Belle-Île, 47.33° N | 20:00 | 7° | 104 | the crescent lit on the right, tipped a little below level |
| Martinique, 14.6° N | 22:45 | 34° | 129 | the crescent lit from the lower right, half-way to lying on its back |
- Below the horizon the field is still published;
altitudesays whether the moon is up. It is the same geometry carried on under the horizon, so it does not jump at moonrise or moonset. - At new and full moon the direction still exists — it is still where the sun is from the moon — but there is no crescent to show it, and around the instant of the event it swings round within hours.
- It needs the boat's position, not a hemisphere: it is published on the equator, where
litSideis not. - Like
altitudeandazimuth, it is computed for the centre of the Earth, without the moon's parallax.
Which one to use. Use litAngle when the drawing can turn: it shows the moon as it stands in the sky. Use litSide when it cannot — an icon, a calendar — or when the moon is to be drawn upright.
How the two relate. A litAngle between 0 and 180 puts the lit side on the right, between 180 and 360 on the left. With the moon above the horizon, that is the side litSide names for every crescent, quarter and gibbous moon at high latitudes: the unit tests hold the two together at 47° N and 47° S across a lunation. They part where the hemisphere's convention is only an approximation, and more often the lower the latitude: between about 29° N and 29° S the moon can pass on either side of the zenith, and a crescent often lies on its back. At dawn on 7 September 2026 off Martinique the waning crescent is left by the convention and its litAngle is 165: lit from below, a little to the right. They also part, at any latitude, close to the exact new or full moon, when the disc is almost all dark or almost all lit and no side shows.
The moon's path across the sky: track
track is the moon's path for one pass above the horizon, from moonrise to moonset, as a list of points for a map to join. This is the one published at 21:00 UTC on 31 August 2026 off Belle-Île, 46 points, the middle left out:
json
"track": [
{ "time": "2026-08-31T19:54:15Z", "timestamp": 1788206055, "altitude": 0.13, "azimuth": 71.13 },
{ "time": "2026-08-31T20:13:48Z", "timestamp": 1788207228, "altitude": 3.25, "azimuth": 74.53 },
…
{ "time": "2026-09-01T03:04:22Z", "timestamp": 1788231862, "altitude": 57.13, "azimuth": 180.00 },
…
{ "time": "2026-09-01T10:31:52Z", "timestamp": 1788258712, "altitude": 0.12, "azimuth": 294.13 }
]The moon rose an hour before, bearing 071°; it will stand 57° up, due south, at 03:04, and set bearing 294° at 10:32.
| Key | Unit | Notes |
|---|---|---|
time / timestamp | ISO-8601 UTC / epoch seconds | the instant of the point, twice, like every instant of the report |
altitude | degrees | to 0.01° |
azimuth | degrees, 0 = North, 90 = East | to 0.01° |
- Which pass. The one in progress if the moon is up, the next one if it is down. The track therefore stays the same from one moonset to the next: it shows the coming pass while the moon is down, and that same pass, part of it now behind, while the moon is up.
- The first point is that pass's own moonrise and the last its own moonset, with their instants. Both are within a hundredth of a degree of 0.125°, the altitude of the moon's centre at which the library takes it to rise and to set. Every point between them is higher.
- The culmination is one of the points: the moon's passage across the meridian, due south or due north.
- No two points are more than 20 minutes apart, in time order. A pass of 12 to 15 hours is 38 to 48 points; the longest track is 77.
- Each point is the moon's position as
altitudeandazimuthgive it at that instant, to 0.01°.
Drawing it. Place each point on the map as the moon itself is placed from altitude and azimuth, and join the points in order. Do not interpolate between two bearings as numbers: south of the tropics, and within them on some days, the moon culminates in the north and the bearing runs through 0. Off Martinique on 4 September 2026 the moon rises bearing 062°, culminates 77° up due north and sets bearing 299°: the whole pass is in the northern half of the sky, which the rise and set bearings alone do not say.
The track and rise, set, transit. One search finds them all, so where the track and those fields name the same event they give the same instant. While the moon is down, the track's first point is rise, its culmination transit and its last point set. While the moon is up, its last point is set, its culmination is transit until the moon has crossed the meridian, and rise is the moonrise that follows the track's last point: to show when the pass in progress began, read the track's first point.
When there is no track. track is absent when there is no such pass: the moon stays above the horizon for more than 25 hours, or it is below and does not rise within 48 hours. Both happen only above about 61° of latitude, where the moon can circle the pole or stay down for days. A consumer treats the field as optional.
daysToNewMoon and daysToFullMoon
These are found by stepping forward in half-day increments for up to 30 days and taking the first step at which the moon is within about 5 % of new or of full. Two consequences:
- The resolution is half a day. They are for "next full moon is in about nine days", not for scheduling.
- Either field can be absent if no matching step was found inside the 30-day search — which is why the fields are optional.
Accuracy
The positions come from libnova, the LGPL astronomical library, which the daemon links against. The sun's rise, set and transit come from its rise and set routine, and are accurate to roughly a minute for latitudes within about ±72°; the error grows as the sun's path flattens against the horizon at higher latitudes, which is inherent to the problem rather than to the implementation. The twilight bounds and the moon's rise, set and transit are found by the daemon on the computed altitude and hour angle themselves: at each of those instants the sun stands 6°, 12° or 18° below the horizon, or the moon on its rise altitude or on the meridian, to the second.
Atmospheric refraction is handled by the library's standard horizon definition. Real refraction on a given morning depends on the temperature profile over the water and can move an observed sunrise by more than the model's own error, so treat a rise time as good to a minute or two and no better.
The unit tests check the sun against NOAA solar-calculator references for Paris and for the equator at equinox, including the twilight ordering; the moon tests check that phase really is monotonic across a lunation and really does distinguish waxing from waning, and pin phaseName, waxing, age and litSide on the eight phases of the August–September 2026 lunation against the almanac's instants, from a northern and from a southern latitude. litAngle is checked against the direction of the sun from the moon, worked out separately from the altitude and azimuth of the two bodies, at 47° N, off Martinique, on the equator and in the southern hemisphere: the two agree to 0.1°. track is pinned off Belle-Île on 31 August 2026, rise, culmination and set, and checked point by point against the moon's position at the same instants; off Martinique for a pass through the zenith and one through the north; from Sydney; and at 70° N and 78° N for the days with no pass. The moon's rise, set and transit are checked to be the coming event and never a past one, to keep their instant across hourly reports, and to be the track's own instants where the two name the same event; the six twilight bounds are checked against the sun's altitude at those instants.
Consuming the reports
Both payloads use the standard MUXEN envelope:
json
{ "data": { … }, "metadata": { "rxDate": "…", "expireAfterSec": 3600 } }Reading them from a shell:
sh
mosquitto_sub -h 127.0.0.1 -t sextant/sun -C 1 | python3 -m json.tool
mosquitto_sub -h 127.0.0.1 -t sextant/moon -C 1 | python3 -m json.toolFrom a browser, the @muxen/sextant package types both payloads, with every optional field marked optional in TypeScript — which is the cheapest way to be forced to handle the polar cases. phaseName is typed as the union MoonPhaseName and litSide as MoonLitSide. The package marks phaseName, waxing, age and litAngle optional as well, because it also types the messages of a daemon that does not publish them. A point of track is typed as MoonTrackPoint.
Enable --report-astro alone to publish only these two topics; the shipped unit passes --report-all, which includes them.
