feat(train): a ride reads the bike's speed and cadence sensor and power meter (ADR 0030, D49)

11d, its sensors: a ride's Record sheet offers the CSC sensor (0x1816)
and the power meter (0x1818), found nearby and remembered as the strap
is; the person's devices are now one map of DeviceKind to DeviceStore.
The service parses CSC and Cycling Power measurements (JVM-tested,
counters' rollover, coasting), writes the wheel's revolutions, the
crank's cadence and the watts to the file and to series of their own.
The recording screen shows cadence and watts; the page the power's
chart, average, highest and normalised power and the cadence's chart and
average; GPX, TCX and FIT carry them. A plan's watts target reads the
power meter too.

The wheel's speed and distance follow. Not tried with a real sensor.

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-29 15:11:20 +01:00
commit 5c13e6f3c5
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1109,8 +1109,10 @@ void main() {
recording: (
platform: platform,
file: file(),
straps: DeviceStore(File('${folder.path}/strap.json')),
rowers: DeviceStore(File('${folder.path}/rower.json')),
devices: {
for (final kind in DeviceKind.values)
kind: DeviceStore(File('${folder.path}/${kind.name}.json')),
},
announces: AnnounceStore(File('${folder.path}/announce.json')),
),
workouts: workouts,
@ -1167,6 +1169,36 @@ void main() {
});
});
testWidgets("a ride offers the bike's sensors, found nearby and chosen; "
"a run doesn't (D49)", (tester) async {
platform.nearbyBike = [
const FoundDevice(address: 'AA:02', name: 'Assioma', rssi: -50),
];
await withClock(Clock(() => at), () async {
await startRecording(tester);
await tester.tap(find.text('Record'));
await tester.pumpAndSettle();
expect(find.text('Power meter'), findsNothing);
await tester.tap(find.text('Cycling'));
await tester.pump();
expect(find.text('Speed and cadence sensor'), findsOneWidget);
await tester.ensureVisible(find.text('Power meter'));
await tester.pump();
await tester.tap(find.text('Power meter'));
await pass(tester, 2);
await tester.tap(find.text('Assioma'));
await pass(tester, 2);
expect(find.text('Assioma'), findsOneWidget);
await tester.tap(find.text('Start'));
await tester.runAsync(pumpEventQueue);
await pass(tester, 2);
expect(platform.started.single.powerMeter, 'AA:02');
expect(platform.started.single.speedSensor, isNull);
expect(platform.scanned, [DeviceKind.powerMeter]);
});
});
testWidgets('announcements are switched for each activity and '
'remembered, a row offered none (D49)', (tester) async {
await withClock(Clock(() => at), () async {
@ -2054,8 +2086,10 @@ void main() {
: (
platform: FakeRecorderPlatform()..at = at,
file: File('${folder.path}/recording.txt'),
straps: DeviceStore(File('${folder.path}/strap.json')),
rowers: DeviceStore(File('${folder.path}/rower.json')),
devices: {
for (final kind in DeviceKind.values)
kind: DeviceStore(File('${folder.path}/${kind.name}.json')),
},
announces: AnnounceStore(
File('${folder.path}/announce.json'),
),