docs(adr): ADR 0030 (Halkyon Train) and ADR 0031 (the apps' icons)
ADR 0030 records what Train does and where its data comes from: running,
cycling and rowing, in the order recording, intervals, routes, music; the
Polar Loop taken over through Polar's BLE SDK; music from Jellyfin;
devices on phones through flutter_reactive_ble; Health Connect both ways
through the health package; every row sealed; the WP 5000 an FTMS rower,
as scanned; no WaterRower USB driver for now; the Loop's firmware from
Polar directly or through the instance; flutter_map, the instance's map
source, BRouter as a sidecar, offline use and working with CoMaps; and
Train's shell after Todo's.
ADR 0031 gives the apps' icons one family: Lucide glyphs fading from
cobalt through a colour between into each app's own, Learn's graduation
cap, Todo's list, Train's bike, Meditate's circle-dot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 00:43:21 +01:00
|
|
|
|
# ADR 0030: Halkyon Train, what it does and where its data comes from
|
|
|
|
|
|
|
2026-10-28 16:27:12 +01:00
|
|
|
|
- **Status:** decided 27–28 Oct 2026, by the developer (D1–D30 below). The shell (D12)
|
2026-10-28 16:25:28 +01:00
|
|
|
|
is built; recording (D13–D28) is built on Android, iOS waiting for a Mac; Health
|
|
|
|
|
|
Connect (D29) comes next.
|
docs(adr): ADR 0030 (Halkyon Train) and ADR 0031 (the apps' icons)
ADR 0030 records what Train does and where its data comes from: running,
cycling and rowing, in the order recording, intervals, routes, music; the
Polar Loop taken over through Polar's BLE SDK; music from Jellyfin;
devices on phones through flutter_reactive_ble; Health Connect both ways
through the health package; every row sealed; the WP 5000 an FTMS rower,
as scanned; no WaterRower USB driver for now; the Loop's firmware from
Polar directly or through the instance; flutter_map, the instance's map
source, BRouter as a sidecar, offline use and working with CoMaps; and
Train's shell after Todo's.
ADR 0031 gives the apps' icons one family: Lucide glyphs fading from
cobalt through a colour between into each app's own, Learn's graduation
cap, Todo's list, Train's bike, Meditate's circle-dot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 00:43:21 +01:00
|
|
|
|
- **Numbering:** 0025 is held by the federation draft (stashed), 0026–0029 by the .NET
|
|
|
|
|
|
rewrite on the local branch `rewrite-dotnet`, abandoned on 27 Oct; Train takes the next
|
|
|
|
|
|
free number so that neither is ever read as this one.
|
|
|
|
|
|
- **Followed by:** [ADR 0031](0031-the-apps-icons.md) (the apps' icons, Train's among them).
|
|
|
|
|
|
- **Relates to:** ADR 0014 (halkyon_base, which Train's server includes like every other),
|
|
|
|
|
|
ADR 0016 (one account across an operator's services; Train is one of them).
|
|
|
|
|
|
|
|
|
|
|
|
## Context
|
|
|
|
|
|
|
|
|
|
|
|
Train is the third of Halkyon's apps after Learn and Todo, so far only named. On 27 Oct
|
|
|
|
|
|
the developer asked for it to "encompass advanced features for running and cycling", to
|
|
|
|
|
|
connect to a Polar Loop, and for rowing machines' APIs to be looked into (WaterRower,
|
|
|
|
|
|
Kettler, Christopeit, BH Fitness, Tunturi), the Christopeit WP 5000 in detail. For running
|
|
|
|
|
|
they asked for "GPS tracking, automatic stop (*at least* the functionality of e.g.
|
|
|
|
|
|
F-Droid's OpenTracks)", HIIT, music in tandem with the pace, and planning tracks as on
|
|
|
|
|
|
Strava. What was found, 27 Oct:
|
|
|
|
|
|
|
|
|
|
|
|
- **Open Bluetooth profiles carry most of it.** Heart rate (`0x180D`, measurement `0x2A37`
|
|
|
|
|
|
with RR intervals, hence HRV), running speed and cadence (`0x1814`), cycling speed,
|
|
|
|
|
|
cadence and power, and the Fitness Machine Service (FTMS, `0x1826`; Rower Data
|
|
|
|
|
|
`0x2AD1`, Indoor Bike Data, Treadmill Data; resistance set through its control point).
|
|
|
|
|
|
Any strap, footpod, power meter, smart trainer or FTMS rower works without a vendor's
|
|
|
|
|
|
account or SDK, on every platform with Bluetooth LE. Pixels have no ANT+ radio, so on
|
|
|
|
|
|
GrapheneOS Train is Bluetooth LE only.
|
|
|
|
|
|
- **The Polar Loop** (Sept 2025, no screen, no GPS) is no Bluetooth standard: it speaks
|
|
|
|
|
|
Polar's own protocol, and doesn't broadcast its heart rate as a `0x180D` sensor (Polar:
|
|
|
|
|
|
it "cannot be used as a heart rate sensor for other devices"). Polar's BLE SDK
|
|
|
|
|
|
(Android and iOS, its own permissive licence, source-available) covers it: first-time
|
|
|
|
|
|
setup from one's own app (`doFirstTimeUse()`, no Polar account named anywhere), live
|
|
|
|
|
|
HR, acceleration, PPI, skin temperature and PPG, and the export of sleep, sessions,
|
|
|
|
|
|
daily activity, 24/7 HR and PPI and Nightly Recharge. It pairs with one peer only;
|
|
|
|
|
|
re-pairing needs a factory reset. Firmware updates fetch from Polar (a check sending
|
|
|
|
|
|
the device's UUID, firmware version and hardware code, no health data). Polar's cloud
|
|
|
|
|
|
(AccessLink, OAuth) gives much the same through Polar Flow, free for now and revocable.
|
|
|
|
|
|
- **Rowing machines**: none of the five brands publishes an API. WaterRower documents
|
|
|
|
|
|
its S4/S5 monitor's USB serial protocol and its ComModule speaks FTMS; BH's current
|
|
|
|
|
|
i.Concept range speaks FTMS; Kettler's Coach 2 very probably, Christopeit's and
|
|
|
|
|
|
Tunturi's unconfirmed. Concept2's PM5 is documented and speaks FTMS. Older machines on
|
|
|
|
|
|
Classic Bluetooth (SPP) are reachable neither on iOS nor well from Flutter.
|
|
|
|
|
|
- **The Christopeit WP 5000** (manual of 6 Nov 2025): Bluetooth 4.0, a built-in Bluetooth
|
|
|
|
|
|
heart-rate receiver, water with a 16-level magnetic brake the console moves itself (its
|
|
|
|
|
|
12 programmes change it), mains-powered. Its manual names Kinomap, picked by brand, not
|
|
|
|
|
|
as a generic FTMS device; an owner reports (mid-2025) that neither a Garmin's FTMS
|
|
|
|
|
|
rowing profile nor EXR could pair. Christopeit's own app, Christopeit Connect (Vescape,
|
|
|
|
|
|
`com.vescape.christopeit`), supports it, declares no data collected (App Store), and
|
|
|
|
|
|
writes workouts to Health Connect or Apple Health. Whether the console speaks FTMS is
|
|
|
|
|
|
unknown until it is scanned.
|
|
|
|
|
|
- **Health Connect** is part of Android since 14, so GrapheneOS has it without Google Play;
|
|
|
|
|
|
data stays on the device and each app is granted each type. Gadgetbridge (Garmin,
|
|
|
|
|
|
Huawei, Colmi rings, Amazfit and others, no account) and Christopeit Connect write to
|
|
|
|
|
|
it; OpenTracks writes sessions, distance, speed and routes there, but no heart rate.
|
|
|
|
|
|
- **OpenTracks** is Apache-2.0, so its logic may be taken into Train (AGPL) with its
|
|
|
|
|
|
notice; the open projects talking to rowers and trainers (qdomyos-zwift,
|
|
|
|
|
|
OpenRowingMonitor, pirowflo) are GPL, references for the protocols only.
|
|
|
|
|
|
- **Music by tempo**: Spotify closed its tempo data (audio features) to new apps on 27 Nov
|
|
|
|
|
|
2024; Qobuz grants its API to commercial partners only and refuses open-source
|
|
|
|
|
|
integrations. Another Android app's title can be read from its media session and its
|
|
|
|
|
|
tempo looked up (Deezer's `bpm`, often missing; AcousticBrainz, frozen since 2022), but
|
|
|
|
|
|
only shown, never chosen by. Jellyfin's API is open but has no tempo field.
|
|
|
|
|
|
|
|
|
|
|
|
## Decisions (27 Oct 2026, the developer's)
|
|
|
|
|
|
|
|
|
|
|
|
- **D1 What Train does**: running and cycling with "advanced features", rowing machines,
|
|
|
|
|
|
the Polar Loop. For running at least what OpenTracks does (GPS tracking, pausing by
|
|
|
|
|
|
itself), structured intervals (HIIT), music matching the pace, and planning routes as
|
|
|
|
|
|
on Strava.
|
|
|
|
|
|
- **D2 In this order** (recommended): recording first (GPS, auto-pause, a heart-rate
|
|
|
|
|
|
strap), then intervals, then route planning, then music. Recording is what everything
|
|
|
|
|
|
else builds on; route planning is the largest piece (maps and a router); music needs
|
|
|
|
|
|
the intervals' targets to follow.
|
|
|
|
|
|
- **D3 Train takes over the Polar Loop** through Polar's BLE SDK, with no Polar Flow and no
|
|
|
|
|
|
Polar account (recommended, over AccessLink, Polar's cloud, as the main way in): the
|
|
|
|
|
|
SDK gives nearly all AccessLink would, on the device and without Polar in between. Since
|
|
|
|
|
|
the Loop pairs with one peer, the band is Train's alone; the phone holding it syncs it,
|
|
|
|
|
|
and other devices see its data through Train's server, as the SDK runs on Android and
|
|
|
|
|
|
iOS only.
|
|
|
|
|
|
- **D4 Music comes from Jellyfin, with Train as the player** (recommended, over reading
|
|
|
|
|
|
what Spotify or Qobuz play, which the developer judged "a rather stale feature"): Train
|
|
|
|
|
|
chooses the next track by the cadence or pace an interval asks for, may speed a track
|
|
|
|
|
|
up or slow it down a few per cent, and downloads the chosen tracks before a run for
|
|
|
|
|
|
playing offline. Each track's tempo is worked out by Train from its audio, once, and
|
|
|
|
|
|
kept (Jellyfin has no tempo field; its MusicTags plugin only passes on a BPM tag files
|
|
|
|
|
|
already have). A metronome at the target cadence works where no library is set.
|
|
|
|
|
|
|
|
|
|
|
|
- **D5 Devices connect to phones only, through `flutter_reactive_ble`** (BSD-3, Android
|
|
|
|
|
|
and iOS, Philips Hue's; recommended once the desktop was out, over `universal_ble`,
|
|
|
|
|
|
BSD-3 on every platform but younger and less used, and over `flutter_blue_plus`, whose
|
|
|
|
|
|
own licence asks a fee for commercial use, which an AGPL app can't pass on): "I find it
|
|
|
|
|
|
exceedingly unlikely one owns or purchases a fitness band or smartwatch without
|
|
|
|
|
|
intending or even owning a smartphone", and "the open license is also of paramount
|
|
|
|
|
|
importance". Train talks to its own small interface (scan, connect, subscribe, write),
|
|
|
|
|
|
the profiles' parsers are pure Dart, tested on recorded frames, so another library can
|
|
|
|
|
|
take its place behind it.
|
|
|
|
|
|
|
|
|
|
|
|
- **D6 Health Connect both ways** (recommended): Train writes its workouts there (the
|
|
|
|
|
|
session with its type, route, heart rate, distance, speed and calories), off until the
|
|
|
|
|
|
person turns it on, its own store staying the source of truth; and reads from there
|
|
|
|
|
|
other apps' workout sessions (their heart rate, distance, power) and the person's weight
|
|
|
|
|
|
and resting heart rate, each source turned on by itself. Sources Train can't speak to
|
|
|
|
|
|
(Garmin and Colmi through Gadgetbridge, the WP 5000 through Christopeit Connect) come
|
|
|
|
|
|
in so; Train tells its own records by their origin and takes a session another app
|
|
|
|
|
|
wrote of a workout it recorded itself for the same one. Through the `health` package
|
|
|
|
|
|
(MIT, CARP's, Android and iOS; recommended was a Kotlin channel of our own): it has no
|
|
|
|
|
|
types for power or cadence, which Train therefore neither writes nor reads there, but
|
|
|
|
|
|
records from the sensors directly; they can be added upstream or by a channel of our
|
|
|
|
|
|
own when a source needs them.
|
|
|
|
|
|
- **D7 Train's data is sealed, always** (recommended): the payloads of its rows (sessions,
|
|
|
|
|
|
routes, every series, what is imported) are ADR 0017's sealed content under the
|
|
|
|
|
|
person's data key, as ADR 0019 seals a private studyset, so the instance syncs what it
|
|
|
|
|
|
can't read; imported data syncs as Train's own does. Health data is a special category
|
|
|
|
|
|
of the GDPR (Art. 9), and nothing on the server needs to read it. The instance still
|
|
|
|
|
|
sees that a row exists, its size, and when it changed; what Train works out (training
|
|
|
|
|
|
load, zones, records) is worked out on the device; a route shared later is a copy made
|
|
|
|
|
|
readable on purpose, as publishing is in Learn.
|
|
|
|
|
|
|
|
|
|
|
|
- **D8 The WP 5000 is an FTMS rower, reached by Train's FTMS client and nothing of its
|
|
|
|
|
|
own** (found on the developer's machine, 27 Oct, with nRF Connect): its console
|
|
|
|
|
|
(`MRK-950R-8021`) advertises the Fitness Machine service, with its service data saying
|
|
|
|
|
|
"rower, available" (`0x1826`, `01 10 00`), and offers Fitness Machine Feature, Status,
|
|
|
|
|
|
Training Status, the Control Point (write, indicate), Rower Data (notify) and Supported
|
|
|
|
|
|
Resistance Level Range. Its features: cadence, total distance, pace, resistance level,
|
|
|
|
|
|
stroke count, energy, heart rate, elapsed time, power, and setting a resistance target.
|
|
|
|
|
|
A frame while rowing: 25 strokes a minute, 6 strokes, 18 m, pace 439 s, 100 W,
|
|
|
|
|
|
resistance 1, 1 kcal, heart rate 0 (no strap), 17 s. The forum's report that Garmin and
|
|
|
|
|
|
EXR couldn't pair isn't explained by the machine as it is now (an older firmware, or a
|
|
|
|
|
|
console already connected elsewhere, which most allow one of, may be). Its other
|
|
|
|
|
|
services (`0xFFF0`, `0xF100`, `0xAE00`, and one whose UUID spells "YULU" and "MERACH",
|
|
|
|
|
|
presumably the console's maker's own protocol, which Kinomap and Christopeit Connect
|
|
|
|
|
|
may use) are left alone; Health Connect and working out a protocol aren't needed.
|
|
|
|
|
|
|
|
|
|
|
|
- **D9 No WaterRower USB driver for now** (recommended): it would serve S4 monitors
|
|
|
|
|
|
without the ComModule, on Android only (iOS allows no USB serial device outside Apple's
|
|
|
|
|
|
accessory programme), through an OTG cable during the workout, as a second transport
|
|
|
|
|
|
beside Bluetooth, with no WaterRower at hand to test on; a WaterRower with the
|
|
|
|
|
|
ComModule speaks FTMS. Its protocol is published, so a driver can be added behind
|
|
|
|
|
|
Train's sensor interface once someone asks for it with one at hand.
|
|
|
|
|
|
|
|
|
|
|
|
- **D10 The Loop's firmware: from Polar directly by default, or through the instance**
|
|
|
|
|
|
(the developer's choice, after option 1 alone was recommended for phones always on a
|
|
|
|
|
|
VPN; over updating never). Polar's SDK checks with Polar's Firmware Management API
|
|
|
|
|
|
(sending a UUID made from the band's ID, its firmware version, its hardware code and
|
|
|
|
|
|
a fixed client name, no health data) and flashes either what it fetched or a file on
|
|
|
|
|
|
the phone (`updateFirmware(identifier, firmwareUrl)` takes `file://`, read in its
|
|
|
|
|
|
source, 27 Oct); the band's settings are backed up over Bluetooth into the phone's
|
|
|
|
|
|
memory and written back, never sent anywhere. So:
|
|
|
|
|
|
- **Directly** (the default): the phone asks Polar and fetches the file. Polar learns
|
|
|
|
|
|
the phone's address, a VPN's exit if one is always on (on GrapheneOS with "Block
|
|
|
|
|
|
connections without VPN").
|
|
|
|
|
|
- **Through the instance** (a setting): the phone sends the band's UUID, firmware
|
|
|
|
|
|
version and hardware code to Train's server, which asks Polar, fetches the file once
|
|
|
|
|
|
for every band of that hardware and hands it to the phone, which flashes it from a
|
|
|
|
|
|
file. Polar learns the instance's address, which on a small instance points at its
|
|
|
|
|
|
operator nearly as well as a home's does.
|
|
|
|
|
|
Train says both where the setting is, and names what Polar learns where it offers an
|
|
|
|
|
|
update. An update never starts by itself: Train first syncs what is on the band and
|
|
|
|
|
|
deletes it there, says the band is erased and set up again, and asks.
|
|
|
|
|
|
|
|
|
|
|
|
- **D11 Maps and routes** (recommended, after looking at how open apps do it; 27 Oct):
|
|
|
|
|
|
- **The map widget is `flutter_map`** (BSD-3, Fleaflet's; the developer's choice, over
|
|
|
|
|
|
`maplibre`, BSD-3 and rendering vector tiles natively but at 0.3): drawing tracks,
|
|
|
|
|
|
heart rate's colours and waypoints over the map is ordinary Flutter. Vector tiles
|
|
|
|
|
|
come through `vector_map_tiles` and its PMTiles plugin, tested on the developer's
|
|
|
|
|
|
phone with a region's file before route planning is built; should it stutter,
|
|
|
|
|
|
`maplibre` takes the map screens' place.
|
|
|
|
|
|
- **The map's source is the instance's, one of three** (an operator's setting): a PMTiles
|
|
|
|
|
|
file of its region as a sidecar (recommended, seen by no one else); OpenFreeMap (no
|
|
|
|
|
|
key, no contract; the default without a sidecar); or a provider of the operator's
|
|
|
|
|
|
choice with its address and key (MapTiler, JawgMaps and the European others), whose
|
|
|
|
|
|
key is then readable by anyone using the app. OpenStreetMap's own tile servers forbid
|
|
|
|
|
|
such use. The app names the source in use and what it sees.
|
|
|
|
|
|
- **The router is BRouter, as a sidecar** (MIT; recommended over GraphHopper, which needs
|
|
|
|
|
|
gigabytes to import a country, and Valhalla, with fewer sport profiles): its own image,
|
|
|
|
|
|
128–512 MB, the 5°×5° segments of the operator's region downloaded from BRouter's
|
|
|
|
|
|
project (built from OSM with elevation) and refreshed monthly, its profiles text files
|
|
|
|
|
|
a running one can be written for. As LanguageTool: no site of its own, reached by
|
|
|
|
|
|
Train's server only, for people signed in, limited, no waypoint kept or logged. A
|
|
|
|
|
|
route saved is Train's data, sealed (D7). Searching for places (geocoding) is
|
|
|
|
|
|
decided when planning is built; until then a route is planned by tapping the map.
|
|
|
|
|
|
- **Offline**: recording, following a route (synced beforehand with BRouter's turn
|
|
|
|
|
|
hints), noticing a detour and guiding back to the route, music. Maps offline along a
|
|
|
|
|
|
route (a corridor of a few kilometres downloaded with it), whole regions optional:
|
|
|
|
|
|
a phone with CoMaps holds them already. Planning, rerouting and searching need the
|
|
|
|
|
|
instance; BRouter on Android (its app, or its engine inside Train) may come later,
|
|
|
|
|
|
never on iOS (no Java).
|
|
|
|
|
|
- **With CoMaps** (Apache-2.0, OSM's data; read in its source at `63a53413`, 27 Oct, by
|
|
|
|
|
|
an agent, nothing tried on a device): its maps (MWM, its own format, in its private
|
|
|
|
|
|
storage) can't be read by another app, so the apps work together instead:
|
|
|
|
|
|
1. a route planned in Train goes to CoMaps as GPX (VIEW or SEND, a new list each;
|
|
|
|
|
|
tracks, routes, waypoints, elevation, times, colours kept; no GeoJSON), to be
|
|
|
|
|
|
seen there offline;
|
|
|
|
|
|
2. turn hints come from Train, which announces them with the workout's own, since
|
|
|
|
|
|
CoMaps navigates along no imported track (its issue #251, open);
|
|
|
|
|
|
3. optional: a route to a point is handed to CoMaps (`cm://route?sll=…&saddr=…&dll=…
|
|
|
|
|
|
&daddr=…&type=pedestrian|bicycle`), which builds it offline and the person starts;
|
|
|
|
|
|
on Android, Train may read along through CoMaps' navigation provider (since mid
|
|
|
|
|
|
2026; a runtime permission the person grants), for the route and what is left;
|
|
|
|
|
|
4. tracks and routes from CoMaps reach Train as GPX when the person shares them.
|
|
|
|
|
|
Train builds the `cm://` links itself: Organic Maps' API library and `om://` don't
|
|
|
|
|
|
reach CoMaps on Android. On iOS (`iphone/Maps/CoMaps.plist`, `DeepLinkHandler.swift`,
|
|
|
|
|
|
read 28 Oct): GPX, KML and KMZ open in CoMaps (1, 4), `cm://route` works with one
|
|
|
|
|
|
point between start and finish (3), its `backurl` leads back to Train; picking a
|
|
|
|
|
|
point (`crosshair`) is "not supported on iOS", and iOS has no navigation provider.
|
|
|
|
|
|
|
|
|
|
|
|
- **D12 Train's shell is Todo's** (recommended, 27 Oct; the icon the developer's choice,
|
|
|
|
|
|
the rest taken without objection on 28 Oct):
|
|
|
|
|
|
- **a server of its own**, `apps/train/` with `halkyon_train_server`, `_client`,
|
|
|
|
|
|
`_flutter`, including halkyon_base (`trainBase()`), its own register when it runs
|
|
|
|
|
|
alone and a member of the instance's where the operator names one (ADR 0016); over
|
|
|
|
|
|
Train's endpoints in Learn's server, whose deploys, migrations and failures would
|
|
|
|
|
|
then be Train's, and which an operator couldn't run Train without;
|
|
|
|
|
|
- **synced with `serverpod_offline_sync`** (ADR 0001), its rows sealed (D7), since
|
|
|
|
|
|
recording happens offline;
|
|
|
|
|
|
- **ports of its own in development**, 8180–8191 where Learn and Todo share
|
|
|
|
|
|
8080–8091, so that Train runs beside Learn's server as a member of its register;
|
|
|
|
|
|
- **Android and iOS** record; **Linux, macOS and Windows** show and plan (D5); no web,
|
|
|
|
|
|
as for Learn and Todo, whose keys (ADR 0017) need libsodium natively;
|
|
|
|
|
|
- **Halkyon Train**, `app.halkyon.train`, in English and German alike;
|
|
|
|
|
|
- **its icon is Lucide's `bike`**, in the family of [ADR 0031](0031-the-apps-icons.md)
|
|
|
|
|
|
("activity" and "heart-pulse" look like basic health apps, "a defibrillator or ADAC
|
|
|
|
|
|
health application"; "this is more about training"; Lucide has no runner);
|
|
|
|
|
|
- **deployed once recording works** and the developer wants to use it, not as an
|
|
|
|
|
|
empty shell first.
|
|
|
|
|
|
|
2026-10-28 11:04:53 +01:00
|
|
|
|
## Recording (28 Oct 2026, the developer's)
|
|
|
|
|
|
|
|
|
|
|
|
- **D13 Positions are recorded by Train's own platform code** (recommended, over the
|
|
|
|
|
|
`geolocator` package and paid background-tracking packages): on Android a foreground
|
|
|
|
|
|
service of its own, as Learn's Pomodoro timer has, reading `LocationManager`'s GPS
|
|
|
|
|
|
provider as OpenTracks does and appending every fix to a recording file in the app's
|
|
|
|
|
|
folder, which the app follows while open and takes in when it opens again; on iOS
|
|
|
|
|
|
CoreLocation with background location; both behind one interface (`TrackRecorder`),
|
|
|
|
|
|
faked in tests. So a recording goes on with the app swiped away, and nothing of Google
|
|
|
|
|
|
Play services is in the app: `geolocator`'s Android part depends on Play services'
|
|
|
|
|
|
location library even where its `forceLocationManager` keeps it from being used, a
|
|
|
|
|
|
non-free library that F-Droid's main repository refuses, and its positions arrive in
|
|
|
|
|
|
Dart, which a swipe ends. The strap and the rower join the same service later.
|
|
|
|
|
|
- **D14 A workout and its series are kept apart** (recommended, over one row holding
|
|
|
|
|
|
every sample and over sealed GPX or FIT files in the instance's storage): `workout`
|
|
|
|
|
|
(type, start, end, name, notes, totals; changed when edited, the last writer winning, as
|
|
|
|
|
|
a studyset's payload) and `workout_series` (a chunk of one series, positions, heart rate,
|
|
|
|
|
|
strokes, pauses, of up to 10 minutes; written once as the workout is saved, never
|
|
|
|
|
|
changed, deleted with its workout, as Learn's review log), every payload sealed (D7)
|
|
|
|
|
|
and nothing readable in a column, not the type nor the start. Editing a workout touches
|
|
|
|
|
|
one small row, two devices never conflict over samples, and every source is one more
|
|
|
|
|
|
kind of series. A chunk's payload is JSON as every payload, each column (time,
|
|
|
|
|
|
latitude, longitude, altitude, accuracy, …) a list of whole-number differences from the
|
|
|
|
|
|
value before, gzipped before it is sealed, since sealed bytes compress no more. The
|
|
|
|
|
|
recording file is the device's own and never synced. UUID v7 ids carry their creation
|
|
|
|
|
|
time, which the sync engine's change times tell anyway (D7's list).
|
|
|
|
|
|
- **D15 Sealing: the envelope moves into halkyon_app, Train holds its key itself**
|
|
|
|
|
|
(recommended, over moving all of Learn's key management and over a copy of the
|
|
|
|
|
|
envelope in Train): Learn's `PayloadEnvelope` (a payload sealed for its table, row and
|
|
|
|
|
|
column; readable payloads signed by the account; what doesn't open `Locked`) moves
|
|
|
|
|
|
unchanged into halkyon_app, Learn importing it from there; Train has one key per person
|
|
|
|
|
|
in a synced `content_key` row, its sealed ID saying it is the person's, and a device
|
|
|
|
|
|
without the account's keys writes nothing, as in Learn. Learn's tree (private or
|
|
|
|
|
|
readable studysets, their records, rewrites) stays Learn's. Publishing a workout later
|
|
|
|
|
|
doesn't change it (asked by the developer): a published workout is a readable copy made
|
|
|
|
|
|
on the device (D7), which may leave out the heart rate or the first and last few hundred
|
|
|
|
|
|
metres, as Strava's privacy zones do, the sealed workout staying as it is; the envelope
|
|
|
|
|
|
can sign the copy with the account's key.
|
|
|
|
|
|
- **D16 Auto-pause by each activity's best signal, never discarding samples**
|
|
|
|
|
|
(recommended, over GPS alone and over manual pauses only): a pause, automatic or by
|
|
|
|
|
|
hand, is a marker in a series of its own, and moving time, pace and the totals leave
|
|
|
|
|
|
paused stretches out, so a misjudged pause is corrected from the data afterwards.
|
|
|
|
|
|
Running and walking stop with the phone's step detector (Android's step sensor, iOS's
|
|
|
|
|
|
pedometer; its permission asked for, GPS alone without it), confirmed by GPS; cycling
|
|
|
|
|
|
by GPS speed, paused below 3 km/h for 5 s and going on above 5 km/h; rowing by the
|
|
|
|
|
|
rower's stroke rate, no stroke for 5 s. On by default for running and cycling, the
|
|
|
|
|
|
thresholds adjustable once Train has settings.
|
|
|
|
|
|
- **D17 Running, walking, hiking, cycling and indoor rowing first, in this order of
|
|
|
|
|
|
parts** (recommended): 5a the envelope moved, Train's key holder, the two models and
|
|
|
|
|
|
their migration, the chunk codec and the first synced repository; 5b recording on
|
|
|
|
|
|
Android (the service, the file taken in after a swipe, the recording screen,
|
|
|
|
|
|
auto-pause, saving); 5c the workouts and a workout's page with its track on a map
|
|
|
|
|
|
(D11's `flutter_map` and the instance's tile source brought forward from route
|
|
|
|
|
|
planning, the developer's choice: a run without its track looks unfinished), totals,
|
|
|
|
|
|
splits and charts; 5d recording on iOS; 5e the heart-rate strap; 5f the WP 5000 over
|
|
|
|
|
|
FTMS, from frames recorded with nRF Connect. Health Connect (D6) and the Loop (D3)
|
|
|
|
|
|
follow recording. Each type maps onto one of Health Connect's exercise types, and is a
|
|
|
|
|
|
name in the sealed payload, so new ones need no migration.
|
feat(train): a recording's file read and saved as a workout (ADR 0030, D18, D20)
The recorder's file is text, one sample a line (D18): positions with
their accuracy, heart rates, steps, pauses and the end. Reading it drops
a last line cut short and skips any line it can't read; a file without
its end is a recording cut off, which ends at its last sample.
A recording becomes its workout and series: every fix kept with its
accuracy (D20), the distance through those within 20 m and never across
a pause, the moving time without the pauses (D16), each kind of sample
in chunks of ten minutes (D14). D18 to D20 are recorded in ADR 0030.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 12:31:09 +01:00
|
|
|
|
- **D18 The recording file is text, one sample a line, appended** (recommended, over
|
|
|
|
|
|
fixed-size binary records, smaller only while recording, as a saved workout is
|
|
|
|
|
|
delta-coded JSON anyway, and over samples handed straight to Dart, lost when Android
|
|
|
|
|
|
stops the Flutter engine while the service runs, against D13): each line a sample with
|
|
|
|
|
|
its kind (a position, a heart rate, a step, a pause's start or end) and its time,
|
|
|
|
|
|
flushed every few seconds. A line cut short is dropped as it is read; an unfinished
|
|
|
|
|
|
file the app finds when it next opens is offered to be saved up to its last sample or
|
|
|
|
|
|
thrown away. It lies in the person's folder, in the app's private storage, unsealed,
|
|
|
|
|
|
as their local database does, until it is saved as a workout and sealed (D7).
|
|
|
|
|
|
- **D19 Permissions asked when first needed, never background location**
|
|
|
|
|
|
(recommended, over background location, which would let a recording start with Train
|
|
|
|
|
|
closed and go on after Android killed its process, but asks to be allowed all the time
|
|
|
|
|
|
on a settings page of its own since Android 11 and a declaration on Google Play, more
|
|
|
|
|
|
than Train's privacy fits; OpenTracks does without it too): precise location and, on
|
|
|
|
|
|
Android 13 and later, notifications as an activity with GPS first starts, not at
|
|
|
|
|
|
install or first launch (refused notifications hide the recording's notification and
|
|
|
|
|
|
leave it running); physical activity only as running, walking or hiking first starts
|
|
|
|
|
|
with auto-pause on, for the step detector (D16), auto-pause falling back to GPS speed
|
|
|
|
|
|
for that workout where it is refused. The service is a foreground service of the types
|
|
|
|
|
|
`location` and `health`, started from the app while it is open, which keeps its access
|
|
|
|
|
|
to location with the screen off; a process Android kills leaves its file (D18) to be
|
|
|
|
|
|
found when Train next opens.
|
|
|
|
|
|
- **D20 Every fix is kept with its accuracy, and filtered as it is read**
|
|
|
|
|
|
(recommended, over dropping imprecise fixes in the service, which keeps the series
|
|
|
|
|
|
smaller and the reading simpler but can't be undone for the workouts recorded under a
|
|
|
|
|
|
threshold that proves wrong): the file and the saved series hold each fix the phone
|
|
|
|
|
|
delivered with the accuracy it reported; distance, pace, splits and the track drawn
|
|
|
|
|
|
leave out the fixes less accurate than a threshold, 20 m to start with (as OpenTracks'
|
|
|
|
|
|
default), adjustable once Train has settings. A better filter later (smoothing, a
|
|
|
|
|
|
Kalman filter) reads old workouts anew, as nothing was thrown away. An hour at a fix a
|
|
|
|
|
|
second is 3,600 fixes, tens of kilobytes delta-coded and gzipped.
|
feat(train): recording a workout on Android (ADR 0030, D13, D16, D19, D21)
RecorderService, a foreground service of the types location and health
started from the open app, takes fixes from LocationManager's GPS and
steps from the step detector, pauses by itself (AutoPause, tested on the
JVM) and appends each sample to the person's recording file, flushed
every five seconds. MainActivity asks for location, notifications and,
for auto-pause on foot, physical activity, never background location
(D19), and bridges the service to the app.
The app follows the file's lines with RecordingBloc and shows the moving
time, the distance, the pace or speed and GPS's state; the notification
shows the time and the distance with Pause or Resume. Stop is held down;
a stopped recording is saved as a workout and its series, or discarded
after asking, and one cut off or left unsaved is offered as the app
opens. Home has the Record button where the device records.
Three choices of the implementation's are recorded under D21 for review:
rowing waits for 5f, auto-pause on foot without steps goes by GPS speed
(below 2 km/h, on above 3 km/h), and Stop's hint shows above the button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 13:24:56 +01:00
|
|
|
|
- **D21 The recording screen** (proposed, the developer's): home's Record button opens a
|
|
|
|
|
|
sheet of the activities, the last one recorded chosen, with auto-pause's switch as D16
|
|
|
|
|
|
sets it; Start asks the permissions (D19). Until the first fix precise enough the
|
|
|
|
|
|
screen says it waits for GPS, showing the accuracy, while it records already, the time
|
|
|
|
|
|
from Start. Recording, it shows the moving time, the distance and the pace now (min/km
|
|
|
|
|
|
running, walking and hiking, km/h cycling; rowing time alone until 5f; heart rate once
|
|
|
|
|
|
a strap is there, 5e), an automatic pause told apart from one by hand, no map until 5c.
|
|
|
|
|
|
Stop is a long press, so that no pocket ends a run (over a tap and a confirmation,
|
|
|
|
|
|
quicker and easier to hit by mistake), then Save or Discard, Discard asked again. The
|
|
|
|
|
|
notification shows time and distance with Pause or Resume and opens the app; stopping
|
|
|
|
|
|
only in the app, so that no stray touch on the lock screen ends a workout. A recording
|
|
|
|
|
|
found cut off as Train opens (D18) is offered to be saved up to its last sample or
|
|
|
|
|
|
discarded.
|
|
|
|
|
|
|
|
|
|
|
|
Built on 28 Oct with three choices of the implementation's, for the developer to look
|
|
|
|
|
|
at: rowing indoors isn't offered yet, as a foreground service on Android 14 and later
|
|
|
|
|
|
needs a type its permissions allow and rowing without the rower has none (5f's
|
|
|
|
|
|
Bluetooth gives it `connectedDevice`); on foot without the step detector auto-pause
|
|
|
|
|
|
goes by GPS speed below 2 km/h for 5 s, going on above 3 km/h (D16 names only the
|
|
|
|
|
|
bike's thresholds); and Stop's hint shows above the button for three seconds rather
|
|
|
|
|
|
than in a snack bar, which would cover the button it speaks of. The service holds a
|
|
|
|
|
|
partial wake lock while it records (at most 12 hours), so that its clock and file run
|
|
|
|
|
|
with the screen off where no GPS fix wakes the processor.
|
2026-10-28 13:42:28 +01:00
|
|
|
|
- **D22 OpenFreeMap first, the PMTiles sidecar with route planning** (recommended, over
|
|
|
|
|
|
the sidecar now, which no one else ever sees the areas viewed through but needs Train's
|
|
|
|
|
|
server and the sidecar deployed on learn.halkyon.eu before anything else of Train is
|
|
|
|
|
|
ready, and a region chosen, gigabytes on the server's disk; and over a source fixed in
|
|
|
|
|
|
the app, which skips D11's operator's setting and would be undone): Train's server names
|
|
|
|
|
|
the instance's map source, as its operator sets it, OpenFreeMap where nothing is set
|
|
|
|
|
|
(D11), so 5c needs no deploy; a workout's page names the source drawing it and what it
|
|
|
|
|
|
sees (the device's address and the area viewed, not the track). The sidecar comes when
|
|
|
|
|
|
route planning gives Train's server a reason to run on the instance: the region's
|
feat(train): the workouts listed, and each one's page with its map (ADR 0030, D22-D24)
Home lists the person's workouts newest first under their months, each
with its activity's icon, its name (else the time of day's and the
activity's), when it started, its distance, moving time and pace or
speed; those this device can't open come last, locked. A workout's page
shows its track on the instance's map source (flutter_map with
vector_map_tiles, pinned at 9.0.0-beta.13), in pieces between its
pauses, start and end marked; its totals with the elevation gained;
its splits per kilometre on foot, per 5 km cycling; pace or speed and
elevation over the distance, drawn by the app's own painter; and what
the map's source sees. Its name and notes are edited there; deleting
asks first and can be undone for a moment from home (halkyon_app's
DelayedDeletions, TrainPerson.deletions).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 14:29:39 +01:00
|
|
|
|
PMTiles file served as ordinary tiles (D23).
|
|
|
|
|
|
- **D23 `flutter_map` 8 with `vector_map_tiles` 9's beta** (recommended; 28 Oct): D11's
|
|
|
|
|
|
combination doesn't exist as released, as `vector_map_tiles`' stable line (8) needs
|
|
|
|
|
|
`flutter_map` 7 and its PMTiles plugin (1.5.0, October 2024) `vector_map_tiles` 8,
|
|
|
|
|
|
while the 9 line, beta since November 2024 (beta.13 on 1 September 2026), is where its
|
|
|
|
|
|
maintainer works and needs `flutter_map` 8. Over the stable line, all three packages
|
|
|
|
|
|
of 2024 and held there until 9 is out, and over `maplibre` (0.3.7), native and smooth
|
|
|
|
|
|
but drawing tracks and their colours as its layers rather than Flutter's widgets, the
|
|
|
|
|
|
reason for D11's choice. The beta is pinned exactly and moves to 9.0.0 once released;
|
|
|
|
|
|
the sidecar serves the region's PMTiles file as ordinary vector tiles with Protomaps'
|
|
|
|
|
|
`pmtiles serve` (go-pmtiles, BSD-3-Clause) instead of the app reading it, so the app
|
|
|
|
|
|
holds nothing of PMTiles'.
|
|
|
|
|
|
- **D24 The workouts and a workout's page** (the developer's; 28 Oct): home lists
|
|
|
|
|
|
the workouts newest first under their months, each with its activity's icon, its name
|
|
|
|
|
|
(else the time of day's and the activity's, "Morning run"), its day and start, distance,
|
|
|
|
|
|
moving time and pace (km/h cycling); one this device can't open shows locked; Record
|
|
|
|
|
|
stays. A workout's page: the map on its upper part, the track in cobalt, its pauses
|
|
|
|
|
|
gaps rather than straight jumps, start and end marked, the source's credit on it, fit
|
|
|
|
|
|
to the track (heart rate's colours with the strap, 5e); its totals (distance, moving
|
|
|
|
|
|
time, pace or speed, the whole time, the elevation gained, counting climbs above 3 m
|
|
|
|
|
|
of the fixes precise enough, as GPS's altitude is noisy); its splits, per kilometre on
|
|
|
|
|
|
foot, per 5 km cycling; charts of pace or speed and of elevation over the distance,
|
|
|
|
|
|
drawn by the app's own small painter (over `fl_chart`, MIT, larger than two lines
|
|
|
|
|
|
need and one more dependency to follow); under the map, which source draws it and what
|
|
|
|
|
|
that sees, with its page where the operator names one (D22). Its name and notes are
|
|
|
|
|
|
edited; deleting asks first and offers an undo, as Learn's deletions do. Without GPS no
|
|
|
|
|
|
map and no splits.
|
|
|
|
|
|
|
|
|
|
|
|
Built so (28 Oct), with choices of mine for the developer to look at: the day's parts
|
|
|
|
|
|
start at 5, 12, 17 and 22 on the device's clock; locked workouts come last under no
|
|
|
|
|
|
month, as when they were is sealed too; the map stays fixed above the page as it
|
|
|
|
|
|
scrolls, pans and zooms but doesn't turn, and folds the source's credit into
|
|
|
|
|
|
flutter_map's attribution button, the credit being wider than a phone; start green,
|
|
|
|
|
|
end cerise, the track cobalt in either theme (the map's colours are the source's);
|
|
|
|
|
|
the charts show their lowest and highest value beside them and nothing else; where
|
|
|
|
|
|
the instance can't be asked for its map source, the page says so and shows the rest;
|
|
|
|
|
|
the elevation counts a climb once it passes 3 m above where it last turned, so a
|
|
|
|
|
|
steady climb's last metres short of the next 3 are not counted (a 20 m hill reads as
|
|
|
|
|
|
18 or 19 m): the simple filter, to be compared with recorded runs.
|
feat(train): the heart-rate strap, recorded by the service itself (ADR 0030, D25-D27)
The Record sheet gains a Heart rate row: a sheet looks for straps
advertising the Heart Rate Service, the nearest first, once nearby
devices are allowed, and the one chosen is remembered in the person's
folder; it records with every workout unless switched off for one.
The recorder service connects to it with Android's own Bluetooth API,
no package (D26), parses Heart Rate Measurement in Kotlin (tested on
the JVM), writes each rate and the strap's state to the recording
file, and asks for a lost strap again for as long as it records. The
recording screen shows the heart rate, and "Strap lost" while it is
gone; a workout's page shows the heart's average and highest rate,
each split's average and its chart.
iOS's recording waits for a Mac (D25), after the rower.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 15:50:36 +01:00
|
|
|
|
- **D25 iOS recording waits for a Mac** (the developer's; 28 Oct): 5e and 5f come before
|
|
|
|
|
|
5d, which moves to the end of step 5. iOS's side (CoreLocation with background
|
|
|
|
|
|
location, the pedometer, a Live Activity for the notification) needs Xcode, which runs
|
|
|
|
|
|
on macOS alone, and this machine has none; written unbuilt it would look done and not
|
|
|
|
|
|
compile, and CI's runner has no Xcode either. The heart-rate strap and the WP 5000 are
|
|
|
|
|
|
built and tested on Android here meanwhile.
|
|
|
|
|
|
- **D26 The recorder service talks to the strap itself** (recommended, the developer's;
|
|
|
|
|
|
28 Oct; over `flutter_reactive_ble` for finding the strap beside it, two stacks touching
|
|
|
|
|
|
one strap for one screen's sake, and over D5's Dart alone, whose heart rate would stop
|
|
|
|
|
|
where a swipe stops the Flutter engine, D18, while GPS records on): Android's own
|
|
|
|
|
|
Bluetooth API in Kotlin, as OpenTracks has it, connects during a recording and writes
|
|
|
|
|
|
the `h` lines, so heart rate is recorded as long as positions are; finding and choosing
|
|
|
|
|
|
a strap goes through the recorder's channel too, so Train has one Bluetooth stack and
|
|
|
|
|
|
no Bluetooth package. This revises D5: its open licence holds, as nothing third-party
|
|
|
|
|
|
is left, and the platform's API sits behind `RecorderPlatform` as the library would
|
|
|
|
|
|
have behind Train's own interface. The profiles' parsers (Heart Rate Measurement now,
|
|
|
|
|
|
FTMS's Rower Data in 5f) are Kotlin, tested on the JVM on recorded frames, and iOS (5d)
|
|
|
|
|
|
needs them again in Swift.
|
|
|
|
|
|
- **D27 The strap, from choosing it to the workout's page** (the developer's; 28 Oct): the
|
|
|
|
|
|
Record sheet's *Heart rate* row names the strap or "No strap" and opens a sheet that
|
|
|
|
|
|
looks for devices advertising the Heart Rate Service (0x180D), the nearest first;
|
|
|
|
|
|
the one chosen is remembered on the device, in the person's folder (`strap.json`, its
|
|
|
|
|
|
address and name, never synced: each phone finds it anew), *Forget strap* letting it
|
|
|
|
|
|
go. Nearby devices are asked for as the sheet first looks (D19), declared never to
|
|
|
|
|
|
locate (Android 12 on; before, scanning needs location). No bonding: the service
|
|
|
|
|
|
connects by address. A remembered strap records with every workout unless switched
|
|
|
|
|
|
off on the sheet for one; the recording doesn't wait for it. The recording screen
|
|
|
|
|
|
shows the heart rate ("–" while connecting), and "Strap lost" where GPS's state shows
|
|
|
|
|
|
while it is gone; the notification stays time and distance, as a rate changing each
|
|
|
|
|
|
second would repost it each second. A lost strap is asked for again in the background
|
|
|
|
|
|
for as long as the recording runs; the minutes without it stay a gap. Only the rate is
|
|
|
|
|
|
kept (`h` lines), not the R-R intervals, until something uses them. The workout's
|
|
|
|
|
|
page: the heart's average and highest rate while moving among the totals, each
|
|
|
|
|
|
split's average, and its chart over the distance (over the moving time without a
|
|
|
|
|
|
track). The track is coloured by heart-rate zones once Train's settings hold the
|
|
|
|
|
|
person's maximum rate, as colouring by each workout's own range paints an easy run
|
|
|
|
|
|
as red as a race.
|
|
|
|
|
|
|
|
|
|
|
|
Built so (28 Oct), with choices of mine for the developer to look at: the file gains
|
|
|
|
|
|
`strap <time> connecting|connected|lost` lines (the format's version stays 1, as a
|
|
|
|
|
|
reader before skipped lines it didn't know); a rate older than 5 s shows as "–"; the
|
|
|
|
|
|
first attempt connects directly, falling back on Android's background connection
|
|
|
|
|
|
once it fails and after every loss; a sighting stronger than −65 dBm is "close by";
|
|
|
|
|
|
the heart-rate chart averages the rates of each stretch (as the pace chart's
|
|
|
|
|
|
stretches, or 10 s, at least, over the time). Not yet tried with a real strap.
|
feat(train): rowing on the WP 5000 over FTMS (ADR 0030, D28)
Rowing indoors is offered again, with the rower chosen on the Record
sheet: a sheet looks for Fitness Machine Service rowers, and the one
chosen is remembered in the person's folder. The recorder service
connects to it as to the strap (one NotifyLink for both), parses
FTMS's Rower Data in Kotlin (tested on the JVM on frames written from
the specification and D8's), counts its totals from its first frame,
pauses 5 s after the last stroke, and gets the connectedDevice type. A
row's screen shows its metres, split per 500 m, stroke rate and watts;
its page its average split, stroke rate and power, the highest power,
its strokes, splits per 500 m and charts of the split, stroke rate and
power. Not yet tried on the WP 5000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 16:20:37 +01:00
|
|
|
|
- **D28 The rower, from choosing it to the workout's page** (the developer's; 28 Oct): with
|
|
|
|
|
|
indoor rowing chosen, the Record sheet's *Rowing machine* row opens a sheet as the
|
|
|
|
|
|
strap's, looking for the Fitness Machine Service (0x1826) and listing the machines
|
|
|
|
|
|
whose advertisement says "rower"; the one chosen is remembered in the person's folder
|
|
|
|
|
|
(`rower.json`). Indoor rowing is offered again, Start asking for a rower. Train reads
|
|
|
|
|
|
Rower Data and takes no control of the console (its resistance through the Control
|
|
|
|
|
|
Point waits for a reason, as training plans); a console held by another app (one
|
|
|
|
|
|
connection at a time) is said so. The foreground service gets the `connectedDevice`
|
|
|
|
|
|
type, which nearby devices allow. Each frame becomes a line `r <time> <stroke rate>
|
|
|
|
|
|
<strokes> <metres> <watts>` and a `rowing` series; the console's running totals count
|
|
|
|
|
|
from its first frame, and its own heart rate is kept only without a strap.
|
|
|
|
|
|
Auto-pause: no new stroke for 5 s pauses (D16), the next one goes on, on by default.
|
|
|
|
|
|
The recording screen: moving time, metres, the split per 500 m of the last 30 s,
|
|
|
|
|
|
the stroke rate, watts, the heart rate with a strap. The list shows the split per
|
|
|
|
|
|
500 m; the page: distance, moving time, the average split, stroke rate and watts,
|
|
|
|
|
|
the highest watts, strokes, heart rate; splits per 500 m with their split, stroke rate
|
|
|
|
|
|
and watts; charts of the split, the stroke rate and watts over the distance; no map.
|
|
|
|
|
|
Written from FTMS 1.0 and D8's frame, checked on the developer's machine later (an
|
|
|
|
|
|
nRF Connect log of Rower Data and Fitness Machine Status, or a first row).
|
|
|
|
|
|
|
|
|
|
|
|
Built so (28 Oct), with choices of mine for the developer to look at: a machine whose
|
|
|
|
|
|
advertisement carries no Fitness Machine data is listed too, as it can't tell, and only
|
|
|
|
|
|
one that says it is something else is left out; a console reset to zero mid-row counts
|
|
|
|
|
|
on from there; metres rowed while paused count for nothing, as GPS's do (D16); the
|
|
|
|
|
|
strap's sheet and the rower's are one sheet, the same "close by" (−65 dBm); the
|
|
|
|
|
|
strap and the rower share one connection class (`NotifyLink.kt`), and a row asks for
|
|
|
|
|
|
nearby devices as it starts, as a run asks for location, since Android needs it for
|
|
|
|
|
|
the service's type; Fitness Machine Status (the console's own pause and reset) is
|
|
|
|
|
|
not read yet, the strokes deciding the pause; the notification shows a row's
|
|
|
|
|
|
distance in kilometres, as the others'. Not yet tried on the WP 5000.
|
2026-10-28 11:04:53 +01:00
|
|
|
|
|
2026-10-28 16:25:28 +01:00
|
|
|
|
## After recording (28 Oct 2026, the developer's)
|
|
|
|
|
|
|
|
|
|
|
|
- **D29 Health Connect next, the Loop after it** (recommended, the developer's; over the
|
|
|
|
|
|
Loop first, and over neither before the intervals): Health Connect is the smaller
|
|
|
|
|
|
piece and serves every workout Train records already; writing gives them to every
|
|
|
|
|
|
other app on the phone, reading brings what Train can't speak to itself (Gadgetbridge's
|
|
|
|
|
|
watches and rings, Christopeit Connect's rows), and the Loop's sessions can later go
|
|
|
|
|
|
there the same way. The Loop needs Polar's SDK, its first-time setup and D10's
|
|
|
|
|
|
firmware, and new kinds of data (daily activity, sleep, 24/7 heart rate, Nightly
|
|
|
|
|
|
Recharge) with synced models of their own; it is tried only with the band at hand.
|
|
|
|
|
|
The intervals (D2) need neither.
|
2026-10-28 16:27:12 +01:00
|
|
|
|
- **D30 Health Connect through Kotlin of our own** (recommended, the developer's; revises
|
|
|
|
|
|
D6's `health` package, as D26 revised D5): `androidx.health.connect:connect-client`
|
|
|
|
|
|
(Apache-2.0, AndroidX, no Play services) behind a channel of Train's, as the strap's.
|
|
|
|
|
|
The `health` package (13.3.2) lacks power, cadence, elevation gained, distance and
|
|
|
|
|
|
speed as records of their own, ties no heart rate to its session, has no changes API
|
|
|
|
|
|
(so each read would be a whole window read again) and sets no client record ID (so a
|
|
|
|
|
|
workout written again would be a second copy, not a replacement). Our own gets every
|
|
|
|
|
|
record type, the changes API, Train's workout ID as the client record ID (written
|
|
|
|
|
|
again replaces, deleted deletes) and each record's origin. Its logic is tested on the
|
|
|
|
|
|
JVM, the Dart side on a fake platform; Apple Health needs Swift of its own later,
|
|
|
|
|
|
as iOS waits for a Mac anyway (D25).
|
2026-10-28 16:25:28 +01:00
|
|
|
|
|
docs(adr): ADR 0030 (Halkyon Train) and ADR 0031 (the apps' icons)
ADR 0030 records what Train does and where its data comes from: running,
cycling and rowing, in the order recording, intervals, routes, music; the
Polar Loop taken over through Polar's BLE SDK; music from Jellyfin;
devices on phones through flutter_reactive_ble; Health Connect both ways
through the health package; every row sealed; the WP 5000 an FTMS rower,
as scanned; no WaterRower USB driver for now; the Loop's firmware from
Polar directly or through the instance; flutter_map, the instance's map
source, BRouter as a sidecar, offline use and working with CoMaps; and
Train's shell after Todo's.
ADR 0031 gives the apps' icons one family: Lucide glyphs fading from
cobalt through a colour between into each app's own, Learn's graduation
cap, Todo's list, Train's bike, Meditate's circle-dot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-28 00:43:21 +01:00
|
|
|
|
## Consequences
|
|
|
|
|
|
|
|
|
|
|
|
- Train's sensors are one pattern: open profiles read by the app itself, on the phone;
|
|
|
|
|
|
the Loop through Polar's SDK, also on the phone.
|
|
|
|
|
|
- The recording runs in an Android foreground service from its first version, so that it
|
|
|
|
|
|
survives the screen going off.
|
|
|
|
|
|
- The Loop can't be used with Polar Flow while Train holds it; going back needs a factory
|
|
|
|
|
|
reset of the band.
|
|
|
|
|
|
- The desktop connects to no device and records nothing; it shows and plans, with what
|
|
|
|
|
|
the phones recorded coming through the server.
|