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 cancelwhile running,BACK to closewhen finished. - START is ignored on both views.
- The save view ends the app with
System.exit().
Wear OS¶
- Hint:
Swipe to cancelwhile running,Swipe to closewhen 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 connectedtext: every network failure readsNo internet.
Apple Watch¶
⏳ Same views; after the save view the app returns to the start list instead of ending (port plan).
Privacy consent¶
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 deviceor fromUpload trackswhile 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
401keeps it. - Recording, the queue and the FIT activity do not depend on the consent.
Consent 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 |
|---|---|---|---|---|
| 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): titlePrivacy, the textSend your trips to sailever.com?andsailever.com/privacybelow it, thenNoandYes. 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.
- 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).
- The view shows the code and starts polling every 5 s with the poll token from the response.
- When a poll answers
pairedwith a device token, the watch stores the token and showsPaired. - 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 +
pairedis ignored and polling goes on. - After
Pairedthe 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 tracksis 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 tracksshowsPair device firstand 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 deviceopens 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 synctext 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).