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

View file

@ -12,6 +12,7 @@ class FakeRecorderPlatform({
RecorderPermission.location,
RecorderPermission.notifications,
RecorderPermission.activity,
RecorderPermission.bluetooth,
},
}) implements RecorderPlatform {
/// What was asked for, each time.
@ -19,7 +20,22 @@ class FakeRecorderPlatform({
/// How each recording was started.
final started =
<({String path, Activity activity, AutoPauseBy? autoPause})>[];
<
({
String path,
Activity activity,
AutoPauseBy? autoPause,
String? strap,
})
>[];
/// The straps nearby, seen as soon as a scan starts, and whether
/// Bluetooth is on.
List<FoundStrap> nearby = [];
bool bluetoothOn = true;
/// How many scans were started.
int scans = 0;
/// The notices shown, the first one the start's.
final notices = <RecorderNotice>[];
@ -55,8 +71,14 @@ class FakeRecorderPlatform({
required Activity activity,
required AutoPauseBy? autoPause,
required RecorderNotice notice,
String? strap,
}) async {
started.add((path: path, activity: activity, autoPause: autoPause));
started.add((
path: path,
activity: activity,
autoPause: autoPause,
strap: strap,
));
notices.add(notice);
_file = File(path)
..writeAsStringSync(
@ -102,4 +124,14 @@ class FakeRecorderPlatform({
_file!.readAsLinesSync().forEach(controller.add);
return controller.stream;
}
@override
Stream<FoundStrap> scanStraps() {
scans++;
if (!bluetoothOn) {
return Stream.error(BluetoothOff());
}
// Open until cancelled, as a scan is.
return Stream.multi((listener) => nearby.forEach(listener.add));
}
}