Skip to content

Sailing calculations

Garmin WatchApp Wear OS WatchApp Apple Watch Garmin DataField Server
Status ✅ ✅ ⏳ 🟡 (COG and baro arrow only) 🟡 (own rules, see below)
Code source/CogCalculator.mc, source/BaroIndicator.mc, source/SunCalc.mc, source/MoonCalc.mc, source/DataModel.mc, source/TripContext.mc core/…/CogCalculator.kt, core/…/BaroIndicator.kt, core/…/SunCalc.kt (SunCalc, MoonCalc), wear/…/recording/TripEngine.kt — (SailEverCore) source/CogCalculator.mc, source/BaroIndicator.mc, source/DataModel.mc app/services/sailever/sltb/kinematics.rb, app/services/ingestion/derivation.rb, app/services/ingestion/track_recompute.rb, app/services/trip_bridges/rebuild.rb, app/services/engine_detector.rb

Verified against code: 2026-10-11

Purpose

The sailing maths that more than one product uses. Each section says the rule, then which products implement it and where they differ. Where a value is shown is in the screen specs.

Units

  • 1 nm = 1852 m. Knots = m/s × 3600 / 1852.
  • Pressure in hPa (sensors give Pa on Garmin, hPa on Wear OS, kPa on Apple).
  • Angles in whole degrees [0, 360), 0 = north, clockwise.

COG and turn detection

Source. COG is the GPS direction of travel, never the compass. It is used only at SOG ≥ 1.0 kn and with GPS quality ≥ 2; below that the GPS course is noise or switches to the compass.

Smoothing. The shown COG is the circular mean of the last 5 headings (1 Hz, about 5 s). Angles are averaged as unit vectors (sum of sin, sum of cos, then atan2), so 350° and 10° give 0°, not 180°. Result truncated to whole degrees.

Reset. Whenever there is no valid heading (too slow, no fix), COG shows nothing, the turn angle is 0, and the whole history is dropped. Neither the mean nor a turn spans a gap.

Turn detection (tack or jibe): - The last 30 raw headings are kept (1 Hz, about 30 s). No turn until 30 are in. - before = circular mean of the oldest two, after = circular mean of the newest two, each rounded to whole degrees. - Turn = shortest signed difference after − before, in [−180, 180]; positive = clockwise (to starboard). - Reported only when |turn| > 30°, otherwise 0.

The track view shows the turn: ../watchapp/track-view.md. The DataField shows it in its own layout: ../datafield/screen.md.

Product Heading source Differences
Garmin WatchApp Position.Info.heading — (reference)
Wear OS WatchApp Location.bearing, or derived from the previous fix when missing (../watchapp/recording.md) GPS quality is mapped from accuracy in metres
DataField Position.Info.heading same results; older code layout
Server bearing between neighbouring points no smoothing, no turns; see SOG and COG on the server
Apple Watch ⏳ —

Barometer trend

Sampling. Pressure is read every second, but only kept as a trend sample at an interval that grows with the time since recording began:

Phase (time since start) Sample every Max samples Span Sensitivity
0–30 min 2 min 15 30 min 0.2 hPa
30–120 min 5 min 18 90 min 0.4 hPa
from 120 min 10 min 36 6 h 0.6 hPa

Each sample keeps the sensitivity of its phase. When the buffer is full, the oldest sample goes. The first sample is taken one interval after start. Samples are raw readings, not smoothed.

Arrow. Compares the newest sample with every older one: - With fewer than 3 samples: sideways, blue. - For older sample i (0 = oldest) of n: threshold = sensitivity of the newest sample × (0.7 + 0.3 × (i + 1) / (n − 1)). Newer samples get a higher threshold. - Count the older samples clearly below (newest − older > threshold) and clearly above (< −threshold). - ≥ 60 % below → arrow up; ≥ 60 % above → arrow down. From 80 % the arrow is red, otherwise yellow. - Otherwise sideways, blue.

Arrow colours: red 0xFF0000, yellow 0xFFAA00, blue 0x00AAFF (Connect IQ named colours). Shapes: up = triangle pointing up; down = triangle pointing down; sideways = triangle pointing right, half the height.

Storm drop (WatchApp alert): pressure of the oldest sample − pressure of the newest > limit (user setting, default 1.5 hPa), with at least 2 samples. See ../watchapp/alerts-and-auto-stats.md.

Server storm warning: a different rule. Any two stored points with a baro value at most 3 h apart where the later one is more than 1.5 hPa lower. The first such point gets a storm_warning event and the track a storm flag. The threshold is fixed. SLTB points carry the watch's smoothed pressure (EMA α = 0.2); FIT points carry the raw value.

Product Arrow Storm drop Notes
Garmin WatchApp ✅ ✅ alert phases restart when the app is reopened
Wear OS WatchApp ✅ ✅ alert monotonic clock; no barometer → no samples
DataField ✅ — phases count from when the DataField is loaded
Server — ✅ own 3 h rule —
Apple Watch ⏳ ⏳ CMAltimeter

Sunrise and sunset

The NOAA low-precision solar algorithm, computed on the watch from the position and the local date. No network. Good to about a minute at sailing latitudes.

  • Zenith 90.833° (refraction and the sun's radius).
  • Day of year from the local calendar date.
  • The UTC result is shifted by the watch's current time-zone offset.
  • When the sun does not cross the horizon (midnight sun, polar night) there is no time.
  • Text: HH:MM, both zero-padded, minutes truncated (not rounded); --:-- when there is no time.

Implemented in Garmin and Wear OS WatchApp (identical). Not in the DataField or the server. Apple ⏳. Display: ../watchapp/environment.md.

Moon phase

  • Phase fraction = (days since the new moon of 2024-01-11 11:57 UTC, Unix 1704974220) / 29.53058867, fractional part, 0 = new moon.
  • Names by fraction:
Fraction Name Short name
< 0.03 or ≥ 0.97 NEW MOON NEW
0.03 – 0.22 WAXING CRESCENT WAX CRESC
0.22 – 0.28 FIRST QUARTER FIRST QTR
0.28 – 0.47 WAXING GIBBOUS WAX GIBB
0.47 – 0.53 FULL MOON FULL
0.53 – 0.72 WANING GIBBOUS WAN GIBB
0.72 – 0.78 LAST QUARTER LAST QTR
0.78 – 0.97 WANING CRESCENT WAN CRESC

Lower bound inclusive. Quarters and full get a narrow band because they are only true for about a day. Short names are for small screens. Implemented in Garmin and Wear OS WatchApp (identical). Apple ⏳.

Distance

Where Rule
WatchApp, Garmin the device odometer of the FIT session (elapsedDistance)
WatchApp, Wear OS computed from fixes: accuracy ≤ 30 m, and speed ≥ 0.3 kn or no speed
DataField Garmin's elapsedDistance of the host activity
Server, track END distance_m when > 0 and the track was saved (not recovered), or the FIT session distance of an uploaded file, distance_source = device; otherwise the haversine sum over the points (computed)
Server, trimmed track the device distance × (haversine of the kept part / haversine of the whole track), distance_source = device_scaled; computed when there is no device distance
Server, trip sum of the tracks plus the bridges

Details of the watch: ../watchapp/recording.md. Trim (IN / OUT): ../server/track-editor.md; a bridge connects one track's last kept point to the next track's first and counts as distance, never as time; rules: ../server/ingestion.md.

Sailing time and engine time

WatchApp (shown on the watch, sent in END): - Each unpaused second counts for the current vessel state: sailed (Sail), motored (Engine), motorsailed (Motorsail) or stationary (all others). - Distance gains count the same way; stationary distance counts only in the total. - No speed threshold.

Server (logbook numbers) from consecutive point pairs of the kept part: - Pairs more than 300 s apart are skipped (no time, no split distance). - Engine when the first point is engine_on or in Engine / Motorsail: the pair's time and distance are engine. - Otherwise the distance is sailing distance, and the time is sailing time only when the first point's SOG ≥ 0.5 kn. - Sailing and engine distance are scaled by track distance / haversine distance, so they never add up to more than the track. - The server ignores the watch's END time totals.

Rule WatchApp Server
Motorsail own bucket, added to "motored" in trip totals engine
Stationary states own bucket sailing bucket (see Known deviations)
< 0.5 kn counted left out of sailing time

Engine detection

Server only, for the track editor's Analyze track (EngineDetector): a suggestion of where a track was under engine and where under sail, from the positions alone. The skipper checks and corrects it; it is never applied without saving the editor. It reads the speed the way a skipper does: under engine the speed is dead even, and the engine going on or off shows as a step in the speed; under sail the speed varies and builds or fades with the wind.

  1. Positions resampled to every 10 s (linear, not across gaps over 120 s); speed from the positions (central differences, 3-sample mean). Then minutes: the mean speed of each minute.
  2. Variation of each minute: over the ±5 min around it, the speed's standard deviation after removing a straight-line trend, divided by the mean speed (at least 0.5 kn).
  3. Step at each minute: |mean speed of the 5 minutes before − of the 5 after| / the higher of the two (at least 1 kn). A step of 0.375 or more is a speed step (engine on or off).
  4. Per moving minute (mean > 1.5 kn, not in a gap): z = 0.1058 − 1.1858 · (ln(variation + 0.001) + 3.3395) / 1.2219, P = 1 / (1 + e^−z); below about 4 % variation the minute leans towards engine.
  5. A two-state smoother (forward-backward) over the moving minutes, the minute scores raised to the power 0.3. Switching state has a chance of 0.3 per minute within 2 minutes of a speed step and 0.001 elsewhere, so a slow build-up of speed under sail stays sail. Prior 0.3 for engine, 0.8 in the first and last 10 moving minutes (leaving and entering harbour).
  6. The likelier state per minute; runs shorter than 5 min join the one before. The first change is at the track's first point.
  7. Reliability = mean of max(P, 1 − P) over the moving minutes: the expected share of right minutes.

Fitted on 7 days with the skipper's own sail/motor times (a handwritten logbook, Cyclades 2023, Garmin FIT every ~20 s; and the owner's reading of a Mallorca day, 2025, 1 s): 97 % balanced accuracy on days left out of the fit, the reliability matching the observed share. The thresholds were chosen on the same days, so expect less on other boats. Harder cases: motoring into waves, and sailing that starts without a drop in speed. More features (jitter, variation relative to the trip, cruising speed, model wind) did worse. Details: ADR 011.

Average speed

Where Rule
WatchApp track track distance × 3600 / track timer
WatchApp auto-stats interval distance × 3600 / interval seconds
WatchApp state period mean of SOG samples above 0
DataField elapsedDistance / timerTime of the host activity
Server mean SOG of all kept points

Max speed

Where Rule
WatchApp highest 1 Hz SOG, unfiltered
Server highest 30 s rolling mean of point SOG, capped at 999.99 kn

SOG and COG on the server

SLTB carries no speed or course. The server derives them per point (Sailever::Sltb::Kinematics):

  • SOG: central difference: haversine distance from the previous to the next point / time between them. A one-sided difference at track ends and across gaps longer than 3 × the sample interval. A point with no neighbour in reach gets none. Values above 100 kn are dropped as GPS jumps. Then a median-of-3 filter. Rounded to 0.01 kn.
  • COG: initial bearing from the previous to the next point, rounded to 0.1°. None when SOG < 0.3 kn.
  • Earth radius 6 371 008.8 m. Measured against device speed: median difference 0.19 kn.

FIT uploads keep the speed and course from the file.

Known deviations

  • Server sailing bucket includes stationary states. Distance moved at anchor, on a buoy or adrift counts as sailing distance, and time there with SOG ≥ 0.5 kn counts as sailing time. The glossary says sailing time is time under sail.
  • Average and max speed differ between products (table above), so the watch, the DataField and the logbook show different numbers for the same track.
  • Baro storm rule differs between the watch alert (adaptive buffer, user limit) and the server warning (fixed 3 h, 1.5 hPa).