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>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 15:50:36 +01:00
commit de5fd43e06
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 1782 additions and 42 deletions

View file

@ -82,5 +82,23 @@
"workoutMapSource": "Karte von {name}: sieht die Adresse deines Geräts und den gezeigten Ausschnitt, nicht deine Strecke.",
"workoutMapSourceAbout": "Was {name} speichert",
"workoutMapUnavailable": "Die Karte kann gerade nicht geladen werden.",
"workoutNotFound": "Dieses Training gibt es nicht mehr."
"workoutNotFound": "Dieses Training gibt es nicht mehr.",
"recordHeartRate": "Herzfrequenz",
"recordNoStrap": "Kein Brustgurt",
"strapSheetTitle": "Brustgurt",
"strapSearching": "Suche Brustgurte in der Nähe …",
"strapSearchingHint": "Leg den Gurt an: Die meisten wachen erst auf, wenn sie die Haut berühren.",
"strapNear": "ganz nah",
"strapFar": "weiter weg",
"strapUnnamed": "Brustgurt ohne Namen",
"strapForget": "Brustgurt vergessen",
"strapRefused": "Train braucht Geräte in der Nähe, um deinen Brustgurt zu finden. Du kannst das in den Systemeinstellungen erlauben.",
"strapBluetoothOff": "Bluetooth ist aus. Schalte es ein, um deinen Brustgurt zu finden.",
"recordingHeartRate": "Herzfrequenz",
"recordingStrapLost": "Brustgurt nicht verbunden",
"workoutAverageHeartRate": "mittlere Herzfrequenz",
"workoutMaxHeartRate": "höchste Herzfrequenz",
"workoutBpmValue": "{bpm} bpm",
"workoutHeartRateChart": "Herzfrequenz"
}

View file

@ -104,5 +104,32 @@
"workoutMapSourceAbout": "What {name} keeps",
"@workoutMapSourceAbout": {"description": "A link to the map source's page about what it keeps.", "placeholders": {"name": {"type": "String", "example": "OpenFreeMap"}}},
"workoutMapUnavailable": "The map can't be loaded now.",
"workoutNotFound": "This workout is gone."
"workoutNotFound": "This workout is gone.",
"recordHeartRate": "Heart rate",
"recordNoStrap": "No strap",
"@recordNoStrap": {"description": "On the Record sheet, under Heart rate, where no strap is chosen."},
"strapSheetTitle": "Heart-rate strap",
"strapSearching": "Looking for straps nearby…",
"strapSearchingHint": "Put the strap on: most wake up only once they touch skin.",
"strapNear": "close by",
"@strapNear": {"description": "Under a strap found: its signal is strong."},
"strapFar": "further off",
"@strapFar": {"description": "Under a strap found: its signal is weak."},
"strapUnnamed": "Unnamed strap",
"@strapUnnamed": {"description": "A strap that sends no name."},
"strapForget": "Forget strap",
"strapRefused": "Train needs nearby devices to find your strap. You can allow it in the system's settings.",
"strapBluetoothOff": "Bluetooth is off. Turn it on to find your strap.",
"recordingHeartRate": "heart rate",
"@recordingHeartRate": {"description": "Under the heart rate, lower case."},
"recordingStrapLost": "Strap lost",
"@recordingStrapLost": {"description": "While recording: the strap is out of reach or off; Train keeps asking for it."},
"workoutAverageHeartRate": "average heart rate",
"@workoutAverageHeartRate": {"description": "Lower case, under a figure."},
"workoutMaxHeartRate": "highest heart rate",
"@workoutMaxHeartRate": {"description": "Lower case, under a figure."},
"workoutBpmValue": "{bpm} bpm",
"@workoutBpmValue": {"placeholders": {"bpm": {"type": "int"}}},
"workoutHeartRateChart": "Heart rate"
}

View file

@ -555,6 +555,108 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'This workout is gone.'**
String get workoutNotFound;
/// No description provided for @recordHeartRate.
///
/// In en, this message translates to:
/// **'Heart rate'**
String get recordHeartRate;
/// On the Record sheet, under Heart rate, where no strap is chosen.
///
/// In en, this message translates to:
/// **'No strap'**
String get recordNoStrap;
/// No description provided for @strapSheetTitle.
///
/// In en, this message translates to:
/// **'Heart-rate strap'**
String get strapSheetTitle;
/// No description provided for @strapSearching.
///
/// In en, this message translates to:
/// **'Looking for straps nearby…'**
String get strapSearching;
/// No description provided for @strapSearchingHint.
///
/// In en, this message translates to:
/// **'Put the strap on: most wake up only once they touch skin.'**
String get strapSearchingHint;
/// Under a strap found: its signal is strong.
///
/// In en, this message translates to:
/// **'close by'**
String get strapNear;
/// Under a strap found: its signal is weak.
///
/// In en, this message translates to:
/// **'further off'**
String get strapFar;
/// A strap that sends no name.
///
/// In en, this message translates to:
/// **'Unnamed strap'**
String get strapUnnamed;
/// No description provided for @strapForget.
///
/// In en, this message translates to:
/// **'Forget strap'**
String get strapForget;
/// No description provided for @strapRefused.
///
/// In en, this message translates to:
/// **'Train needs nearby devices to find your strap. You can allow it in the system\'s settings.'**
String get strapRefused;
/// No description provided for @strapBluetoothOff.
///
/// In en, this message translates to:
/// **'Bluetooth is off. Turn it on to find your strap.'**
String get strapBluetoothOff;
/// Under the heart rate, lower case.
///
/// In en, this message translates to:
/// **'heart rate'**
String get recordingHeartRate;
/// While recording: the strap is out of reach or off; Train keeps asking for it.
///
/// In en, this message translates to:
/// **'Strap lost'**
String get recordingStrapLost;
/// Lower case, under a figure.
///
/// In en, this message translates to:
/// **'average heart rate'**
String get workoutAverageHeartRate;
/// Lower case, under a figure.
///
/// In en, this message translates to:
/// **'highest heart rate'**
String get workoutMaxHeartRate;
/// No description provided for @workoutBpmValue.
///
/// In en, this message translates to:
/// **'{bpm} bpm'**
String workoutBpmValue(int bpm);
/// No description provided for @workoutHeartRateChart.
///
/// In en, this message translates to:
/// **'Heart rate'**
String get workoutHeartRateChart;
}
class _AppLocalizationsDelegate

View file

@ -298,4 +298,60 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get workoutNotFound => 'Dieses Training gibt es nicht mehr.';
@override
String get recordHeartRate => 'Herzfrequenz';
@override
String get recordNoStrap => 'Kein Brustgurt';
@override
String get strapSheetTitle => 'Brustgurt';
@override
String get strapSearching => 'Suche Brustgurte in der Nähe …';
@override
String get strapSearchingHint =>
'Leg den Gurt an: Die meisten wachen erst auf, wenn sie die Haut berühren.';
@override
String get strapNear => 'ganz nah';
@override
String get strapFar => 'weiter weg';
@override
String get strapUnnamed => 'Brustgurt ohne Namen';
@override
String get strapForget => 'Brustgurt vergessen';
@override
String get strapRefused =>
'Train braucht Geräte in der Nähe, um deinen Brustgurt zu finden. Du kannst das in den Systemeinstellungen erlauben.';
@override
String get strapBluetoothOff =>
'Bluetooth ist aus. Schalte es ein, um deinen Brustgurt zu finden.';
@override
String get recordingHeartRate => 'Herzfrequenz';
@override
String get recordingStrapLost => 'Brustgurt nicht verbunden';
@override
String get workoutAverageHeartRate => 'mittlere Herzfrequenz';
@override
String get workoutMaxHeartRate => 'höchste Herzfrequenz';
@override
String workoutBpmValue(int bpm) {
return '$bpm bpm';
}
@override
String get workoutHeartRateChart => 'Herzfrequenz';
}

View file

@ -295,6 +295,62 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get workoutNotFound => 'This workout is gone.';
@override
String get recordHeartRate => 'Heart rate';
@override
String get recordNoStrap => 'No strap';
@override
String get strapSheetTitle => 'Heart-rate strap';
@override
String get strapSearching => 'Looking for straps nearby…';
@override
String get strapSearchingHint =>
'Put the strap on: most wake up only once they touch skin.';
@override
String get strapNear => 'close by';
@override
String get strapFar => 'further off';
@override
String get strapUnnamed => 'Unnamed strap';
@override
String get strapForget => 'Forget strap';
@override
String get strapRefused =>
'Train needs nearby devices to find your strap. You can allow it in the system\'s settings.';
@override
String get strapBluetoothOff =>
'Bluetooth is off. Turn it on to find your strap.';
@override
String get recordingHeartRate => 'heart rate';
@override
String get recordingStrapLost => 'Strap lost';
@override
String get workoutAverageHeartRate => 'average heart rate';
@override
String get workoutMaxHeartRate => 'highest heart rate';
@override
String workoutBpmValue(int bpm) {
return '$bpm bpm';
}
@override
String get workoutHeartRateChart => 'Heart rate';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).