fix(train): a ride by the wheel goes out with its power, cadence and distance, and learns no wheel from tunnels (ADR 0030, D49, D51)

A trainer ride exported heart rates alone (or nothing) and a ride through tunnels the wheel's totals over GPS's distances; GPX's type came before its description and TCX's watts before the cadence, against their schemas. The wheel's size was learnt from rides GPS covered only in part. The live screen parsed the whole recording again on every sensor frame; now it reads each line once and shows sensors on the second's tick. The offline maps' requests time out after 20 s.

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-30 12:55:41 +01:00
commit c8f16686ec
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -307,6 +307,207 @@ void main() {
expect(record[7], 230);
});
group("a ride whose distance is the wheel's (D49)", () {
/// A ride of a minute, the wheel turning 4 times a second (2.1 m round:
/// 8.4 m/s), paused from 20 s to 30 s, the crank at 90, 200 W and a
/// heart rate each second, GPS's fixes those of [fixes].
Recording ride({List<Fix> fixes = const []}) => Recording(
activity: Activity.cycling,
startedAt: start,
fixes: fixes,
wheel: [
for (var s = 0; s <= 60; s++)
Reading(
time: at(s),
value:
(s <= 20
? s
: s < 30
? 20
: s - 10) *
4,
),
],
wheelMm: 2100,
pedalling: [
for (var s = 0; s <= 60; s++) Reading(time: at(s), value: 90),
],
power: [for (var s = 0; s <= 60; s++) Reading(time: at(s), value: 200)],
heartRates: [
for (var s = 0; s <= 60; s++) HeartRate(time: at(s), bpm: 130),
],
pauses: [
PauseMark(time: at(20.5)),
PauseMark(time: at(30), paused: false),
],
endedAt: at(60),
);
test('on a trainer, a point each reading of the wheel while moving, '
"with its way, the crank's cadence and the watts; FIT and TCX carry "
'them, their distances the totals', () {
final (rode, rideSeries) = recordedWorkout(ride());
final rideTrack = exportTrackOf(rode, rideSeries);
// 50 s turning: 200 turns, 420 m; 52 readings while moving.
expect(rode.distanceMetres, 420);
expect(rideTrack.located, isFalse);
expect(ExportFormat.gpx.takes(rideTrack), isFalse);
expect(rideTrack.points, hasLength(52));
expect(rideTrack.points.first.metres, 0);
expect(rideTrack.points[1].metres, closeTo(8.4, 0.01));
expect(rideTrack.points.last.metres, closeTo(420, 0.01));
expect(
rideTrack.points.where((point) => point.afterPause),
hasLength(1),
);
expect(
rideTrack.points.firstWhere((point) => point.afterPause).time,
at(30),
);
for (final point in rideTrack.points) {
expect((point.bpm, point.cadence, point.watts), (130, 90, 200));
}
final tcx = XmlDocument.parse(tcxOf(rode, rideTrack));
final points = tcx.findAllElements('Trackpoint').toList();
expect(points, hasLength(52));
expect(points.last.getElement('DistanceMeters')!.innerText, '420.0');
expect(points.last.getElement('Cadence')!.innerText, '90');
expect(points.last.findAllElements('Watts').single.innerText, '200');
expect(
tcx
.findAllElements('Lap')
.single
.getElement('DistanceMeters')!
.innerText,
'420',
);
final messages = readFit(fitOf(rode, rideTrack));
final records = [
for (final message in messages)
if (message.global == 20) message.fields,
];
expect(records, hasLength(52));
expect(records.last[4], 90);
expect(records.last[5], 42000);
expect(records.last[7], 200);
final session = messages.singleWhere((message) => message.global == 18);
expect(session.fields[9], 42000);
});
test("with GPS short of the wheel's way (a tunnel), the distances are "
"the wheel's, the fixes where it had them", () {
// GPS's first 20 s alone, at the wheel's pace.
final (rode, rideSeries) = recordedWorkout(
ride(fixes: [for (var s = 0; s <= 20; s++) north(s, s * 8.4)]),
);
final rideTrack = exportTrackOf(rode, rideSeries);
expect(rode.distanceMetres, 420);
expect(rideTrack.points, hasLength(52));
expect(rideTrack.points.last.metres, closeTo(420, 0.01));
expect(
[for (final point in rideTrack.points) point.latE7 != null],
[for (var i = 0; i < 52; i++) i <= 20],
);
expect(rideTrack.points[10].metres, closeTo(84, 0.01));
final gpx = XmlDocument.parse(
gpxOfWorkout(rode, rideTrack, name: 'Tunnel'),
);
expect(gpx.findAllElements('trkpt'), hasLength(21));
final records = [
for (final message in readFit(fitOf(rode, rideTrack)))
if (message.global == 20) message.fields,
];
expect(records.last[5], 42000);
});
test("on a trainer without the wheel's sensor, a point each reading of "
'the power meter', () {
final trainer = Recording(
activity: Activity.cycling,
startedAt: start,
pedalling: [
for (var s = 0; s <= 60; s++) Reading(time: at(s), value: 90),
],
power: [
for (var s = 0; s <= 60; s += 2) Reading(time: at(s), value: 200),
],
endedAt: at(60),
);
final (rode, rideSeries) = recordedWorkout(trainer);
final rideTrack = exportTrackOf(rode, rideSeries);
expect(rideTrack.points, hasLength(31));
expect(rideTrack.points.last.cadence, 90);
expect(rideTrack.points.last.watts, 200);
expect(rideTrack.points.last.metres, isNull);
});
test('a ride by GPS keeps its fixes and their way', () {
// GPS all along, at the wheel's pace.
final (rode, rideSeries) = recordedWorkout(
ride(
fixes: [
for (var s = 0; s <= 60; s++)
north(
s,
(s <= 20
? s
: s < 30
? 20
: s - 10) *
8.4,
),
],
),
);
final rideTrack = exportTrackOf(rode, rideSeries);
expect(rideTrack.points.every((point) => point.latE7 != null), isTrue);
expect(
rideTrack.points.last.metres,
closeTo(rode.distanceMetres!, 0.5),
);
});
});
test("GPX: the track's name, description and type in the schema's "
'order', () {
final described = WorkoutContent(
activity: Activity.running,
startedAt: start,
endedAt: at(120),
notes: 'Easy',
);
final gpx = XmlDocument.parse(
gpxOfWorkout(described, track, name: 'Morning run'),
);
expect(
[
for (final element in gpx.findAllElements('trk').single.childElements)
element.localName,
],
['name', 'desc', 'type', 'trkseg', 'trkseg'],
);
});
test("TCX: the extension's RunCadence before the Watts, in the schema's "
'order', () {
final stepped = ExportTrack(
points: [ExportPoint(time: start, cadence: 80, watts: 250)],
laps: [
ExportLap(start: start, end: at(1), movingTime: Duration.zero),
],
);
final tcx = XmlDocument.parse(tcxOf(content, stepped));
expect(
[
for (final element in tcx.findAllElements('TPX').single.childElements)
element.localName,
],
['RunCadence', 'Watts'],
);
});
test('the file is named by when the workout started, in UTC', () {
expect(
exportNameOf(content, ExportFormat.fit),