fix: harden performance launch timing integrity

This commit is contained in:
Developer 2026-09-05 08:18:32 -04:00
parent 8b176293c3
commit d57502c153
2 changed files with 13 additions and 78 deletions

View File

@ -168,21 +168,9 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
} else if (speedMph >= _movementThresholdMph) {
// Movement detected from ready!
if (state.mode == PerformanceLaunchMode.auto) {
// Auto Launch begins
final movementMs = _interpolateMonotonic(
_lastSample?.speed ?? 0.0,
speedMph,
_lastSample?.monotonicTimestampMs ?? (_lastMonotonicMs - 50),
sampleMonotonicMs,
_movementThresholdMph,
);
final movementDt = _interpolateDateTime(
_lastSample?.speed ?? 0.0,
speedMph,
_lastSample?.timestamp ?? sample.timestamp.subtract(const Duration(milliseconds: 50)),
sample.timestamp,
_movementThresholdMph,
);
// Auto Launch begins with authoritative sample timestamp
final movementMs = sampleMonotonicMs;
final movementDt = sample.timestamp;
state = state.copyWith(
state: PerformanceLaunchState.launched,
@ -210,20 +198,8 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
if (speedMph >= _movementThresholdMph) {
// Vehicle moved during countdown!
final movementMs = _interpolateMonotonic(
_lastSample?.speed ?? 0.0,
speedMph,
_lastSample?.monotonicTimestampMs ?? (_lastMonotonicMs - 50),
sampleMonotonicMs,
_movementThresholdMph,
);
final movementDt = _interpolateDateTime(
_lastSample?.speed ?? 0.0,
speedMph,
_lastSample?.timestamp ?? sample.timestamp.subtract(const Duration(milliseconds: 50)),
sample.timestamp,
_movementThresholdMph,
);
final movementMs = sampleMonotonicMs;
final movementDt = sample.timestamp;
final isFalseStart = movementMs < scheduledGo;
@ -270,20 +246,8 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
updatedSplits.every((s) => s.targetSpeedMph != 60.0)) {
final prevSpeed = _lastSample?.speed ?? 0.0;
if (prevSpeed < 60.0 && speedMph >= 60.0) {
final splitMs = _interpolateMonotonic(
prevSpeed,
speedMph,
_lastSample?.monotonicTimestampMs ?? (sampleMonotonicMs - 50),
sampleMonotonicMs,
60.0,
);
final splitDt = _interpolateDateTime(
prevSpeed,
speedMph,
_lastSample?.timestamp ?? sample.timestamp.subtract(const Duration(milliseconds: 50)),
sample.timestamp,
60.0,
);
final splitMs = sampleMonotonicMs;
final splitDt = sample.timestamp;
final splitElapsedSec = (splitMs - movementMs) / 1000.0;
updatedSplits.add(PerformanceSplit(
@ -291,7 +255,7 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
timestamp: splitDt,
monotonicTimestampMs: splitMs,
elapsedFromMovementSec: splitElapsedSec,
speedReachedMph: 60.0,
speedReachedMph: speedMph,
));
}
}
@ -301,27 +265,12 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
if (speedMph >= targetMph && prevSpeed < targetMph) {
// Target speed reached!
final targetMs = _interpolateMonotonic(
prevSpeed,
speedMph,
_lastSample?.monotonicTimestampMs ?? (sampleMonotonicMs - 50),
sampleMonotonicMs,
targetMph,
);
final targetDt = _interpolateDateTime(
prevSpeed,
speedMph,
_lastSample?.timestamp ?? sample.timestamp.subtract(const Duration(milliseconds: 50)),
sample.timestamp,
targetMph,
);
final targetMs = sampleMonotonicMs;
final targetDt = sample.timestamp;
final preciseAccelerationSec = (targetMs - movementMs) / 1000.0;
// Physical plausibility sanity check: 0-60 >= 1.5s, 0-100 >= 3.0s
final minPlausibleSec = state.target == PerformanceLaunchTarget.sixty ? 1.5 : 3.0;
if (preciseAccelerationSec >= minPlausibleSec) {
if (preciseAccelerationSec > 0.0) {
double? reactionSec;
double? totalFromGoSec;
@ -368,7 +317,7 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
telemetryConfidence: confidence,
);
} else {
// Implausible jump -> abort as physical anomaly
// Non-positive time anomaly
state = state.copyWith(
state: PerformanceLaunchState.aborted,
abortReason: PerformanceAbortReason.physicalAnomaly,
@ -405,20 +354,6 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
_lastSample = sample;
}
int _interpolateMonotonic(double v1, double v2, int t1, int t2, double targetV) {
if (v2 <= v1 || t2 <= t1) return t2;
final fraction = ((targetV - v1) / (v2 - v1)).clamp(0.0, 1.0);
final dt = t2 - t1;
return t1 + (dt * fraction).round();
}
DateTime _interpolateDateTime(double v1, double v2, DateTime t1, DateTime t2, double targetV) {
if (v2 <= v1 || t2.isBefore(t1)) return t2;
final fraction = ((targetV - v1) / (v2 - v1)).clamp(0.0, 1.0);
final dtMs = t2.difference(t1).inMilliseconds;
return t1.add(Duration(milliseconds: (dtMs * fraction).round()));
}
Future<void> _persistRun(PerformanceLaunchResult result, int timeMs) async {
try {
await db.addPerformanceRun(

View File

@ -292,7 +292,7 @@ void main() {
expect(stateMachine!.state.state, PerformanceLaunchState.completed);
expect(stateMachine!.state.lastResult!.reactionTimeSec, isNotNull);
expect(stateMachine!.state.lastResult!.reactionTimeSec!, closeTo(0.05, 0.01));
expect(stateMachine!.state.lastResult!.reactionTimeSec!, closeTo(0.25, 0.01));
expect(stateMachine!.state.lastResult!.totalTimeFromGoSec, greaterThan(2.5));
});