Skip to content

Sync and pairing (watch side)

Garmin Wear OS Apple Watch
Status ✅ 🟡 ⏳
Code source/sync/SyncEngine.mc, source/sync/TrackQueue.mc, source/sync/TrackSession.mc, source/views/SyncView.mc, source/ConsentView.mc, source/PairingView.mc, source/DeviceRegistration.mc, source/sync/Net.mc, source/StartMenu.mc, source/TideService.mc, source/TripStore.mc, source/Config.mc wear/…/sync/SyncEngine.kt, wear/…/sync/Http.kt, wear/…/sync/DeviceApi.kt, wear/…/presentation/SailEverApp.kt (SyncScreen, PairingScreen, Screen.Consent, StartList), wear/…/presentation/Menus.kt (MenuScreen body), wear/…/presentation/views/OtherViews.kt (SyncView, PairingView), wear/…/data/TripStore.kt, wear/…/data/TokenCipher.kt, core/…/Consent.kt, core/…/TrackQueue.kt, core/…/TrackSession.kt —

Verified against code: 2026-10-19

Purpose

This file describes what the watch does to get tracks onto sailever.com: when it uploads, what the sailor sees while it does, what happens to files that could not be sent, how the sailor agrees to sending data (privacy consent), and how the watch is paired to an account and unpaired again. It also covers the device registration and the tides that come back with uploads.

The wire protocol (SLTB format, request and response fields, status codes, segment boundaries, queue budget) is in ../shared/track-sync.md. The registration, pairing and poll endpoints are in ../shared/device-api.md.

Principles

  • The watch records first, uploads second. Every closed segment is written to a local file before any request. A failed request never loses data.
  • One attempt, no retries. Each trigger makes one attempt per file. There are no retry loops, backoff timers or background tasks.
  • One request at a time. One app-wide uploader works through its jobs strictly one file at a time, oldest first. The first failure ends the job; the remaining files stay queued. A file already uploaded by an earlier job counts as done.
  • Nothing leaves the watch without consent. Until the sailor agrees on the watch (Privacy consent), the app makes no network request at all: no registration, no pairing request or poll, no upload. Every request goes through one gate that checks the consent.
  • Upload needs pairing. Without a device token (or without consent) every attempt ends at once as "not paired". Recording continues and files queue up regardless.

When uploads happen

Trigger What is sent UI
First segment closes (15 s after the first usable fix, at least 3 positions) That segment, plus any queued discard files None, no beep
Hourly segment close, and a segment closed at the 4 KB size cap That segment, plus any queued discard files None
Pause Nothing. The open segment is closed and queued. Pause view (Garmin) or the app goes to the background (Wear OS) (lifecycle.md)
Stop All queued files of this track Save view, then the app ends
Discard The track's discard file, if one was made (lifecycle.md) None; the start menu opens at once
Upload tracks in the start menu All queued files of all tracks, discard files included Upload view
App launch, crash recovery, stale track Nothing. Recovered and stale tracks are only queued. Upload tracks appears

Segment boundaries: ../shared/track-sync.md.

Response handling

Response Watch action
200, 201 or 202 Deletes the file. Applies the tides in the response (Tides). Continues with the next file.
401 Deletes the device token (the watch is now unpaired). Keeps every queued file. Ends the job.
Any other status, or no response Keeps the file. Ends the job.

After a 401 the start menu shows Pair device, and Upload tracks shows Pair device first.

Queued files ("leftovers")

Files that were not uploaded stay on the watch until an explicit save point: the save view of the next Stop, or Upload tracks. Queued files are never deleted automatically, also not on unpair or 401. Re-pairing uploads them into the account of the new pairing.

  • Visible files: every queued file except discard files. Their count drives Upload tracks (menus.md).
  • Discard files (END reason 1 of a discarded track) are never counted or shown. They ride along with the next upload of any kind except the save view.
  • Above 75 % of the queue budget the start menu sub-label warns with the percentage. When the budget is full, the open track stops writing SLTB; the FINAL segment is still written (../shared/track-sync.md).

Storage location and queue budget per platform: ../shared/track-sync.md.

Save view (after Stop)

Shown right after Stop Track when the device is paired and the stopped track has at least one queued file. It makes one attempt to bring this track's files onto the server. Other tracks in the queue are left for Upload tracks. When it closes, the app ends (lifecycle.md).

When the device is not paired or the track has no files (no fix during the whole track), the app ends at once without this view.

Upload view (Upload tracks)

Opened from Upload tracks when the device is paired. When not paired, the item opens the pairing view instead (or the consent view in its place while there is no consent). It uploads all queued files of all tracks.

The counter talks about tracks, the bar counts files: the counter shows tracks whose queued files are all uploaded, out of the tracks queued at entry; the bar advances per file, so it moves several times during one long track.

Save and upload view layout

Both views share one layout on black. Positions are fractions of screen width (x) and height (y); all text is centred on its y.

Element Position Font / size Colour Content & format Constraints
Title x 0.50, y 0.22 FONT_XTINY (label) Dim 0xAAAAAA Save: Saving track; upload: Uploading tracks —
Progress bar, track centred on x 0.50, top at y 0.42 — Faint 0x555555 Rounded bar, width 0.64, height 0.05 of the screen height (at least 6 px), corner radius half the height —
Progress bar, fill same, from the left — Sail blue 0x00AAFF Width = files done / files at entry; at least as wide as it is high once started; full when there were no countable files —
Status line x 0.50, y 0.62 FONT_TINY (small) See below See below —
Detail line x 0.50, y 0.74 FONT_XTINY (label) Dim 0xAAAAAA See below Only after a failure
Hint x 0.50, y 0.86 FONT_XTINY (label) Faint 0x555555 See platform Upload view only
Phase Status line (colour) Detail line
Save, running Saving to server... (white) —
Upload, running <done> / <total> tracks uploaded (white) —
Save, success Track saved (sail blue) —
Upload, success All tracks uploaded (sail blue) —
Save, failure Error text (engine red 0xFF5555) Saved on watch - sync later; for "Not paired": Saved on watch - pair to sync
Upload, failure Error text (engine red) <done> / <total> tracks uploaded

Error texts:

Text Cause
Not paired No device token
Pairing revoked 401
Phone not connected Garmin only: no Bluetooth connection to the phone, or its request queue is full
No internet No network or no response
Server error - try later Any other HTTP error (400 and up), a request that could not be built, a request or response too large

Timing and closing

View Success Failure Back input while running Back input when finished
Save Shows 2 s, then the app ends Shows 3 s, then the app ends Garmin: the request in flight finishes, no further file is tried, then the app ends. Wear OS: not possible. —
Upload Shows 2 s, then back to the start menu Stays until closed The request in flight finishes (its result counts), then back to the start menu Back to the start menu

Garmin

  • Hint: BACK to cancel while running, BACK to close when finished.
  • START is ignored on both views.
  • The save view ends the app with System.exit().

Wear OS

  • Hint: Swipe to cancel while running, Swipe to close when finished.
  • The save view ends with finish().
  • Every request first asks Wear OS for an internet network and waits up to 10 s for it, then releases it. Without a phone over Bluetooth, Wear OS gives an app Wi-Fi or LTE only on request.
  • There is no Phone not connected text: every network failure reads No internet.

Apple Watch

⏳ Same views; after the save view the app returns to the start list instead of ending (port plan).

Before anything is sent to sailever.com, the watch asks once. The consent is a local gate: it is stored on the watch only (consent_version, current version 1) and is never sent to the server; there is no API field for it. Raising the version asks again (only if the question's meaning changes).

  • When asked: the first time the sailor opens pairing, from Pair device or from Upload tracks while unpaired. The consent view opens in place of the pairing view. It is asked nowhere else.
  • Yes: stores the consent, registers the device if this app version is not registered yet (Device registration), and replaces the consent view with the pairing view, which requests the code.
  • No, or back: stores nothing, sends nothing, back to the start menu. The question comes again the next time pairing is opened.
  • Upgrade: on the first start of a version with consent, a watch that already has a device token counts as consented and the consent is stored (lifecycle.md).
  • Unpair clears the consent together with the token (Unpair). A 401 keeps it.
  • Recording, the queue and the FIT activity do not depend on the consent.

Black background. Positions are fractions of screen width (x) and height (y); text centred on its y.

Element Position Font / size Colour Content
Title x 0.50, y 0.20 FONT_XTINY (label) Dim 0xAAAAAA Privacy
Question, line 1 x 0.50, y 0.42 FONT_TINY (small) White Send your trips to
Question, line 2 x 0.50, y 0.53 FONT_TINY (small) White sailever.com?
Privacy page x 0.50, y 0.70 FONT_XTINY (label) Dim 0xAAAAAA sailever.com/privacy
Hint x 0.50, y 0.86 FONT_XTINY (label) Faint 0x555555 START to answer

The answers are a menu with No first and Yes second, like the discard confirmation (menus.md).

Garmin

  • The view above (system fonts; the numerals font has no letters). START or a tap opens the answer menu (title Privacy).
  • BACK on the view or in the menu acts like No.
  • Fits from 218 × 218 up (checked on 218, 240, 260 and 454 px).

Wear OS

  • One list (MenuScreen): title Privacy, the text Send your trips to sailever.com? and sailever.com/privacy below it, then No and Yes. The Garmin view layout above does not apply.
  • Swipe right acts like No.

Apple Watch

⏳ Same question and answers.

Pairing

Pairing links the watch to a sailever.com account with a 6-digit code shown on the watch (glossary.md). The sailor opens Pair device (or Upload tracks while unpaired), and types the code at sailever.com/pair while signed in. Without consent the consent view opens first; the steps below start after Yes.

  1. On opening, the watch requests a pairing code. The request carries the platform, the device uid, the device model and the app version (../shared/device-api.md).
  2. The view shows the code and starts polling every 5 s with the poll token from the response.
  3. When a poll answers paired with a device token, the watch stores the token and shows Paired.
  4. The start menu then shows Unpair device.

Pairing view layout

Black background. Positions are fractions of screen width (x) and height (y); text centred on its y.

Element Position Font / size Colour Content & format Constraints
Title x 0.50, y 0.20 FONT_XTINY (label) Dim 0xAAAAAA Pair device Always
Code x 0.50, y 0.44 Numerals font (visual-system.md); --- --- and ERR in FONT_TINY (small) on Garmin, whose numerals font has digits only White The code; --- --- until it arrives; ERR when the request failed Before pairing
Website x 0.50, y 0.60 FONT_XTINY (label) Dim 0xAAAAAA sailever.com/pair Before pairing
Waiting x 0.50, y 0.70 FONT_XTINY (label) Faint 0x555555 Waiting... Before pairing
Done x 0.50, y 0.50 FONT_TINY (small) Sail blue 0x00AAFF Paired Replaces code, website and waiting

Garmin

  • The code shows as received: six digits without a space, e.g. 482915.
  • Polling stops once paired. Any poll response other than 200 + paired is ignored and polling goes on.
  • After Paired the view stays until BACK, which returns to the start menu (refreshed).
  • Leaving the view stops polling. Coming back to it requests a new code.

Wear OS

  • The code shows in two groups of three: 482 915.
  • A poll only counts as paired when the response also carries a device token.
  • The view closes by itself 2 s after Paired, back to the start menu. Swipe right closes it at any time.
  • Token storage per platform: ../shared/device-api.md.

Apple Watch

⏳ Same flow; the token is stored in the Keychain (port plan).

Expiry

A code is valid for 10 minutes on the server (../shared/device-api.md). The watch ignores the expiry and the poll interval in the response: after 10 minutes the old code stays on screen and polling goes on until the sailor leaves the view. Leaving and reopening Pair device gets a new code.

Unpair

Unpair device in the start menu acts at once, without confirmation, and on the watch only:

  • The device token and the consent are deleted on the watch.
  • Nothing is sent: the server is not told, and the token stays valid there until it is revoked on the web or the watch is paired again.
  • Queued files are kept. They are sent only after a new consent and a new pairing, into that pairing's account.
  • The item turns into Pair device; opening it asks for consent again.

A token revoked on the web is noticed at the next upload (401, see Response handling).

Device registration

Separate from pairing: once the sailor has agreed (Privacy consent), the watch tells the server that the app is installed, so installs and app versions can be counted. No position or track data is sent. It runs at app start (only with consent) and right after Yes, when this app version is not registered yet. It has no UI: a failure is ignored and retried on the next launch. Without consent nothing is registered. Identity fields per platform (platform, device uid, device model, app version), the endpoint and the client rule: ../shared/device-api.md. Pairing requests carry the same identity fields.

Tides

Tides come back in the response to each successful segment upload, for the position sent with it (../shared/track-sync.md). The watch:

  • Sends its current position with an upload only while a track records and a position is known (not 0, 0).
  • Applies the next high time, next low time and tidal range (one decimal, m) to the Environment view (environment.md).
  • Ignores tides in uploads without a running track (save view after Stop, Upload tracks).

Tides are therefore a paired-watch feature, refreshed 15 s into a track and then hourly.

States and edge cases

  • Silent upload in flight when the save view opens: the save job waits for it.
  • Response lost, file sent twice: the server answers as a duplicate (2xx) and the watch deletes the file.
  • Upload view opened with only discard files queued: not possible; Upload tracks is hidden while only discard files wait.
  • Pairing while a track is paused (after the app was closed): possible from the start menu; files already queued are uploaded with the new token.
  • Fresh install, never paired: no request at all, also not at launch, while recording or on Stop. Files queue up; Upload tracks shows Pair device first and leads to the consent view.
  • Consent given, pairing view left before pairing: the consent stays; next time the pairing view opens directly.
  • 401 after consent: the token is deleted, the consent stays; Pair device opens the pairing view directly.

Known deviations

  • The pairing view ignores code expiry (see Expiry); polling every 5 s also sits exactly at the server's poll rate limit (120 per 10 minutes).
  • Not paired after Stop: the save view's Saved on watch - pair to sync text never shows, because the app ends at once when unpaired.
  • Discard files are not sent by the save view, only by the other uploads.
  • Garmin: FINAL segments are written past the 64 KB budget.
  • Wear OS save view cannot be skipped (Garmin BACK can).