feat(train): elevation from the barometer, its pressure kept raw (ADR 0030, D49)

11c: where a recording has GPS and the phone a barometer, the service
averages its readings over 5 s into the file's `b` lines and the
workout's new `pressure` series (tenths of a pascal). The page, Health
Connect's totals and the exported files take the fixes' altitudes from
it: the standard atmosphere's height at the pressure, levelled by the
median of GPS's altitudes less those; climbs count above 1 m. Workouts
recorded before keep GPS's altitudes; an older app skips the series.

Not tried on a phone yet.

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 14:48:28 +01:00
commit 57cbd97fd1
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_train_flutter/src/feature/plan/model/plan.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/recorded_workout.dart';
@ -131,6 +133,67 @@ void main() {
expect(analysis.elevationGainMetres, 12);
});
group("the barometer's elevation (D49)", () {
/// The pressure, in tenths of a pascal, [metres] above the standard sea
/// level.
int pressureAt(num metres) =>
(1013250 * pow(1 - metres / 44330, 5.255)).round();
test("it gives the climb, its level the GPS altitudes' median", () {
// GPS wobbles by 6 m around 100 m; the air says a steady 20 m climb.
final noisy = [100, 106, 94, 106, 94, 106, 94, 106, 94, 106, 100];
final recording = Recording(
activity: Activity.running,
startedAt: start,
fixes: [
for (final (i, m) in noisy.indexed)
north(i * 10, i * 30, altitudeCm: m * 100),
],
pressures: [
for (var i = 0; i <= 10; i++)
Pressure(time: at(i * 10), decipascals: pressureAt(500 + 2 * i)),
],
endedAt: at(100),
);
final analysis = analysed(recording);
// Each 2 m counts: the barometer's noise is a fraction of a metre.
expect(analysis.elevationGainMetres, 20);
// 500 m by the standard air, moved by the median of GPS's altitudes
// less the barometer's (410 m).
expect(analysis.elevation.first.value, closeTo(90, 0.1));
expect(analysis.elevation.last.value, closeTo(110, 0.1));
final gps = analysed(
Recording(
activity: Activity.running,
startedAt: start,
fixes: recording.fixes,
endedAt: at(100),
),
);
expect(gps.elevationGainMetres, greaterThan(20));
});
test('without GPS altitudes, it stands by the standard air', () {
final analysis = analysed(
Recording(
activity: Activity.running,
startedAt: start,
fixes: [for (var i = 0; i <= 4; i++) north(i * 10, i * 30)],
pressures: [
Pressure(time: at(0), decipascals: pressureAt(300)),
Pressure(time: at(40), decipascals: pressureAt(320)),
],
endedAt: at(40),
),
);
expect(analysis.elevationGainMetres, 20);
// Between two readings, the pressure is taken as it went.
expect(analysis.elevation[2].value, closeTo(310, 0.5));
});
});
test('without altitudes it has no elevation gained, nor its chart', () {
final analysis = analysed(run([north(0, 0), north(60, 200)]));