feat(train): the Loop's live heart rate in a recording (ADR 0030, D41)

A recording with a Loop set up reads its heart rate through Polar's SDK
while no strap sends: the strap first, the Loop where there is none or
while it is lost. The SDK is one for the process, held by the open
app's bridge and by the recording service, so that the band stays
connected with the app gone; the app reopening finds it connected. The
recording file gains `loop` state lines; the rate is kept alone, as `h`
lines, and the Record sheet names the source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 21:32:13 +01:00
commit fa7680821d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,10 +1,10 @@
# ADR 0030: Halkyon Train, what it does and where its data comes from
- **Status:** decided 27–28 Oct 2026, by the developer (D1–D40 below). The shell (D12)
- **Status:** decided 27–28 Oct 2026, by the developer (D1–D41 below). The shell (D12)
is built; recording (D13–D28) is built on Android, iOS waiting for a Mac; Health
Connect (D29–D31) is built; the Loop's setup and keeping (7a, D34–D36) and its
daily data (7b, D37, D38), its sessions (7c, D39) and its firmware (D10, D40) are
built.
built, and its live heart rate (7d, D41).
- **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.
@ -768,6 +768,39 @@ Strava. What was found, 27 Oct:
by itself after its restart, and whether the SDK takes the relayed file as it takes
the one it fetches.
- **D41 The Loop's live heart rate in a recording (7d)** (recommended, the developer's;
28 Oct): Polar's SDK names live heart rate (beats per minute) among the Loop's and
the Polar 360's features (its `Polar360.md`), with PPI, PPG, acceleration and skin
temperature streams. A recording with a Loop remembered takes its heart rate from
the strap where one is remembered and switched on, and from the Loop where there is
none, or while the strap is lost, so that the gap fills (over the strap alone where
remembered, and over choosing one for each recording): a chest strap reads a workout
more truly than the wrist (D32). The recording service reads the Loop through
Polar's SDK (`startHrStreaming`), which already holds the band's connection, the
SDK moving into a holder the open app and the service share, so that the band stays
connected with the screen off and the app gone (over the service's own connection
to the band's Heart Rate Service, as for a strap, a second client on the SDK's
connection, the two fighting over its notifications). It keeps the heart rate
alone, as `h` lines like the strap's (over pulse-to-pulse intervals too, which
nothing uses yet and which cost battery). The Heart rate row names the source.
Built so (28 Oct), with choices of mine for the developer to look at: Polar's SDK is
one for the app's process (`PolarSdk.kt`), held by the open app's bridge and by a
recording reading a Loop, shut down as the last lets go; the background job (D37)
leaves the band alone while either holds it, and the app opening again during a
recording finds the band connected and is told so rather than connecting anew. The
app doesn't disconnect a band a recording reads. The SDK now asks for `FEATURE_HR`
as well. A recording takes the Loop whenever the person has one set up, the strap's
switch on the Record sheet switching off the strap alone (so a strap left at home
is no gap); the Loop's rate is written only while the strap isn't connected, as
`h` lines, the band's state as `loop <time> connecting|connected|lost` lines (the
file's version stays 1, as `strap` lines did), and a rate read off the skin is
none. The stream is asked for again five seconds after it stops (`LoopHeart.kt`,
tested on the JVM). The screen shows *Strap lost* only while the Loop isn't
sending in its place. A rower's own heart rate is kept only where neither a strap
nor a Loop is there. Not yet tried with a Loop: whether its live rate needs the
band in a mode of its own, and how far the wrist's rate lags a strap's.
## Consequences
- Train's sensors are one pattern: open profiles read by the app itself, on the phone;