T03 — telemetry.dart, format.dart, live_telemetry.dart, plus pure domain models (Trip/Segment/TrackPoint/RideStats) with no persistence dependency, so Drift can map to them in T08 rather than the domain depending on the database. T04 — ride_statistics.dart including ElevationAccumulator, ported structurally faithfully: moving average, reversal hysteresis, gainIncludingPending, and the finish() reconciliation against lastRaw. T07 (early, because T04 forced it) — tool/parity/ drives identical fixtures through the real Kotlin files and the Dart port, then diffs. Result: every value byte-identical, including noisy_gain=38.959594555022136 to the last digit. The sole difference is run_avg_speed, where Kotlin's 32-bit Float widens to double with artefacts Dart's binary64 does not reproduce. Documented, not papered over. That harness settled a real question. The ported elevation test failed at 50.9m against Kotlin's 35m bound, which looked like a porting bug. It was not: Kotlin's and Dart's Random(42) are different streams. On a shared LCG fixture both produce 39.0m -- which would also fail Kotlin's own bound. The native guard passes on seed luck rather than on a property of the algorithm. The Dart test now uses the shared LCG, asserts bit-equality with Kotlin, and sets its bound from measured behaviour (25 seeds spanned 24.7-46.7m). 52 tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
2.4 KiB
Dart
81 lines
2.4 KiB
Dart
/// Ephemeral, in-memory state published straight from the location callback.
|
|
///
|
|
/// Ported from `com.rippr.LiveTelemetry` and `com.rippr.UploadStatus`. Both were Kotlin
|
|
/// `object` singletons over `StateFlow`; here they are small value-holding broadcast
|
|
/// streams so the pure layer stays free of Flutter and Riverpod. The UI layer wraps
|
|
/// these in providers rather than the other way round.
|
|
///
|
|
/// Everything in this file is deliberately **not** persisted. Recording state lives in
|
|
/// the database because it must survive process death; these must not, because a stale
|
|
/// value from a previous process would be actively misleading.
|
|
library;
|
|
|
|
import 'dart:async';
|
|
|
|
/// A stream that also remembers its current value, so a late subscriber is not blind
|
|
/// until the next emission.
|
|
class _ValueStream<T> {
|
|
_ValueStream(this._value);
|
|
|
|
final _controller = StreamController<T>.broadcast();
|
|
T _value;
|
|
|
|
T get value => _value;
|
|
|
|
Stream<T> get stream => _controller.stream;
|
|
|
|
void set(T next) {
|
|
_value = next;
|
|
if (!_controller.isClosed) _controller.add(next);
|
|
}
|
|
|
|
Future<void> dispose() => _controller.close();
|
|
}
|
|
|
|
/// The most recent fix, published straight from the location callback.
|
|
///
|
|
/// The Trip row is only written every ~2 s and carries *max* speed, not current, so the
|
|
/// recording screen cannot show a live speedo from it.
|
|
///
|
|
/// This existing precisely because v2.0 shipped max speed as the headline figure and it
|
|
/// read as a frozen screen on a real ride — see `docs/TESTING.md` in the native repo.
|
|
class LiveTelemetry {
|
|
LiveTelemetry._();
|
|
|
|
static final instance = LiveTelemetry._();
|
|
|
|
final _speedKmh = _ValueStream<double>(0.0);
|
|
final _accuracyM = _ValueStream<double>(0.0);
|
|
|
|
double get speedKmh => _speedKmh.value;
|
|
double get accuracyM => _accuracyM.value;
|
|
|
|
Stream<double> get speedStream => _speedKmh.stream;
|
|
Stream<double> get accuracyStream => _accuracyM.stream;
|
|
|
|
void update(double speedKmh, double accuracyM) {
|
|
_speedKmh.set(speedKmh);
|
|
_accuracyM.set(accuracyM);
|
|
}
|
|
|
|
void clear() {
|
|
_speedKmh.set(0.0);
|
|
_accuracyM.set(0.0);
|
|
}
|
|
}
|
|
|
|
/// Last-seen upload failure, surfaced on the recording screen.
|
|
class UploadStatus {
|
|
UploadStatus._();
|
|
|
|
static final instance = UploadStatus._();
|
|
|
|
final _lastError = _ValueStream<String?>(null);
|
|
|
|
String? get lastError => _lastError.value;
|
|
|
|
Stream<String?> get stream => _lastError.stream;
|
|
|
|
void setError(String? message) => _lastError.set(message);
|
|
}
|