feat(train): a ride's distance from the wheel where GPS falls short (ADR 0030, D49)
The speed sensor's wheel gives a ride's distance, splits, progress and live speed where GPS made less than 90 % of its way (a turbo trainer, a tunnel); its circumference starts at 2105 mm, is learnt from rides with 2 km of GPS or typed on the Record sheet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5c13e6f3c5
commit
a848b71bd4
4 changed files with 527 additions and 27 deletions
|
|
@ -30,6 +30,23 @@ void main() {
|
|||
expect(store.read(), isNull);
|
||||
});
|
||||
|
||||
test('a speed sensor keeps its wheel, typed or measured (D49)', () {
|
||||
final store = DeviceStore(File('${folder.path}/speed.json'));
|
||||
const sensor = KnownDevice(address: 'AA:01', name: 'Speed');
|
||||
store.save(sensor.withWheel(2096, typed: true));
|
||||
expect(
|
||||
store.read(),
|
||||
const KnownDevice(
|
||||
address: 'AA:01',
|
||||
name: 'Speed',
|
||||
wheelMm: 2096,
|
||||
wheelTyped: true,
|
||||
),
|
||||
);
|
||||
store.save(sensor.withWheel(null, typed: true));
|
||||
expect(store.read(), sensor);
|
||||
});
|
||||
|
||||
test("a file it can't read is no strap", () {
|
||||
final file = File('${folder.path}/strap.json')..writeAsStringSync('{');
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,51 @@ void main() {
|
|||
expect(series, isEmpty);
|
||||
});
|
||||
|
||||
group("the wheel's distance (D49)", () {
|
||||
/// A ride of [seconds] with GPS [gpsMetres] a second and the wheel
|
||||
/// turning twice a second, its 2.1 m round.
|
||||
Recording ride({required int seconds, required num gpsMetres}) => Recording(
|
||||
activity: Activity.cycling,
|
||||
startedAt: start,
|
||||
fixes: [
|
||||
for (var s = 0; s <= seconds; s += 10)
|
||||
north(s, (s * gpsMetres).round()),
|
||||
],
|
||||
wheel: [
|
||||
for (var s = 0; s <= seconds; s++) Reading(time: at(s), value: s * 2),
|
||||
],
|
||||
wheelMm: 2100,
|
||||
endedAt: at(seconds),
|
||||
);
|
||||
|
||||
test('on a turbo trainer, GPS standing still, the wheel gives it', () {
|
||||
final trainer = ride(seconds: 600, gpsMetres: 0);
|
||||
expect(rodeByWheel(trainer), isTrue);
|
||||
expect(distanceOf(trainer), 2520);
|
||||
final (content, series) = recordedWorkout(trainer);
|
||||
expect(content.distanceMetres, 2520);
|
||||
expect(analyse(content, series).splits, isNotEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'where GPS follows the wheel, GPS gives it, and measures the wheel',
|
||||
() {
|
||||
final road = ride(seconds: 600, gpsMetres: 4.2);
|
||||
expect(rodeByWheel(road), isFalse);
|
||||
expect(distanceOf(road), closeTo(2520, 5));
|
||||
expect(measuredWheelMm(road), closeTo(2100, 5));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'a ride under 2 km, or one whose wheel makes no wheel, measures none',
|
||||
() {
|
||||
expect(measuredWheelMm(ride(seconds: 200, gpsMetres: 4.2)), isNull);
|
||||
expect(measuredWheelMm(ride(seconds: 600, gpsMetres: 8)), isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("the bike's readings are series of their own, read back as they "
|
||||
'were (D49)', () {
|
||||
final wheel = [
|
||||
|
|
|
|||
|
|
@ -544,6 +544,46 @@ void main() {
|
|||
expect(platform.started.last.powerMeter, isNull);
|
||||
});
|
||||
|
||||
test("a ride's GPS measures the speed sensor's wheel, which the next ride "
|
||||
'starts with; one typed stays (D49)', () async {
|
||||
final bloc = await open();
|
||||
bloc.add(
|
||||
const RecordingEventDeviceChosen(
|
||||
DeviceKind.speedSensor,
|
||||
KnownDevice(address: 'AA:01'),
|
||||
),
|
||||
);
|
||||
await bloc.stream.firstWhere((s) => s.speedSensor != null);
|
||||
bloc.add(const RecordingEventStartRequested(Activity.cycling));
|
||||
await bloc.stream.firstWhere((s) => s.phase == RecordingPhase.recording);
|
||||
expect(platform.started.single.wheelMm, defaultWheelMm);
|
||||
platform.write('wheelsize @ 2105');
|
||||
// 2.5 km in 500 s, the wheel turning 1190 times: a 2.1 m wheel.
|
||||
for (var s = 0; s <= 500; s += 10) {
|
||||
platform
|
||||
..now = start.add(Duration(seconds: s))
|
||||
..write('p @ ${(s * 5 * 89.932).round()} 0 500 -')
|
||||
..write('w @ ${(s * 2.38).round()}');
|
||||
}
|
||||
bloc.add(const RecordingEventStopRequested());
|
||||
await bloc.stream.firstWhere((s) => s.phase == RecordingPhase.stopped);
|
||||
bloc.add(const RecordingEventSaveRequested());
|
||||
final saved = await bloc.stream.firstWhere(
|
||||
(s) => s.phase == RecordingPhase.idle,
|
||||
);
|
||||
|
||||
expect(saved.speedSensor!.wheelMm, closeTo(2100, 5));
|
||||
expect(saved.speedSensor!.wheelTyped, isFalse);
|
||||
bloc.add(const RecordingEventWheelSet(2096));
|
||||
final typed = await bloc.stream.firstWhere(
|
||||
(s) => s.speedSensor?.wheelTyped ?? false,
|
||||
);
|
||||
expect(typed.speedSensor!.wheelMm, 2096);
|
||||
bloc.add(const RecordingEventStartRequested(Activity.cycling));
|
||||
await bloc.stream.firstWhere((s) => s.phase == RecordingPhase.recording);
|
||||
expect(platform.started.last.wheelMm, 2096);
|
||||
});
|
||||
|
||||
test('announcements go to the recorder where switched on (D49)', () async {
|
||||
final bloc = await open();
|
||||
Future<void> record(Activity activity, {required bool announce}) async {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ void main() {
|
|||
final recording = readRecording(
|
||||
'$header'
|
||||
'csc ${at(0)} connected\n'
|
||||
'wheelsize ${at(0)} 2105\n'
|
||||
'power ${at(0)} connecting\n'
|
||||
'w ${at(1)} 3\n'
|
||||
'c ${at(1)} 88\n'
|
||||
|
|
@ -31,6 +32,7 @@ void main() {
|
|||
|
||||
DateTime time(int seconds) => start.add(Duration(seconds: seconds));
|
||||
expect(recording.wheel, [Reading(time: time(1), value: 3)]);
|
||||
expect(recording.wheelMm, 2105);
|
||||
expect(recording.pedalling, [Reading(time: time(1), value: 88)]);
|
||||
expect(recording.power, [Reading(time: time(2), value: 240)]);
|
||||
expect(recording.speedSensors, [
|
||||
|
|
|
|||
Loading…
Reference in a new issue