fix: harden performance launch timing integrity
This commit is contained in:
parent
8b176293c3
commit
d57502c153
|
|
@ -168,21 +168,9 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
|
||||||
} else if (speedMph >= _movementThresholdMph) {
|
} else if (speedMph >= _movementThresholdMph) {
|
||||||
// Movement detected from ready!
|
// Movement detected from ready!
|
||||||
if (state.mode == PerformanceLaunchMode.auto) {
|
if (state.mode == PerformanceLaunchMode.auto) {
|
||||||
// Auto Launch begins
|
// Auto Launch begins with authoritative sample timestamp
|
||||||
final movementMs = _interpolateMonotonic(
|
final movementMs = sampleMonotonicMs;
|
||||||
_lastSample?.speed ?? 0.0,
|
final movementDt = sample.timestamp;
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
state: PerformanceLaunchState.launched,
|
state: PerformanceLaunchState.launched,
|
||||||
|
|
@ -210,20 +198,8 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
|
||||||
|
|
||||||
if (speedMph >= _movementThresholdMph) {
|
if (speedMph >= _movementThresholdMph) {
|
||||||
// Vehicle moved during countdown!
|
// Vehicle moved during countdown!
|
||||||
final movementMs = _interpolateMonotonic(
|
final movementMs = sampleMonotonicMs;
|
||||||
_lastSample?.speed ?? 0.0,
|
final movementDt = sample.timestamp;
|
||||||
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 isFalseStart = movementMs < scheduledGo;
|
final isFalseStart = movementMs < scheduledGo;
|
||||||
|
|
||||||
|
|
@ -270,20 +246,8 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
|
||||||
updatedSplits.every((s) => s.targetSpeedMph != 60.0)) {
|
updatedSplits.every((s) => s.targetSpeedMph != 60.0)) {
|
||||||
final prevSpeed = _lastSample?.speed ?? 0.0;
|
final prevSpeed = _lastSample?.speed ?? 0.0;
|
||||||
if (prevSpeed < 60.0 && speedMph >= 60.0) {
|
if (prevSpeed < 60.0 && speedMph >= 60.0) {
|
||||||
final splitMs = _interpolateMonotonic(
|
final splitMs = sampleMonotonicMs;
|
||||||
prevSpeed,
|
final splitDt = sample.timestamp;
|
||||||
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 splitElapsedSec = (splitMs - movementMs) / 1000.0;
|
final splitElapsedSec = (splitMs - movementMs) / 1000.0;
|
||||||
|
|
||||||
updatedSplits.add(PerformanceSplit(
|
updatedSplits.add(PerformanceSplit(
|
||||||
|
|
@ -291,7 +255,7 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
|
||||||
timestamp: splitDt,
|
timestamp: splitDt,
|
||||||
monotonicTimestampMs: splitMs,
|
monotonicTimestampMs: splitMs,
|
||||||
elapsedFromMovementSec: splitElapsedSec,
|
elapsedFromMovementSec: splitElapsedSec,
|
||||||
speedReachedMph: 60.0,
|
speedReachedMph: speedMph,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,27 +265,12 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
|
||||||
|
|
||||||
if (speedMph >= targetMph && prevSpeed < targetMph) {
|
if (speedMph >= targetMph && prevSpeed < targetMph) {
|
||||||
// Target speed reached!
|
// Target speed reached!
|
||||||
final targetMs = _interpolateMonotonic(
|
final targetMs = sampleMonotonicMs;
|
||||||
prevSpeed,
|
final targetDt = sample.timestamp;
|
||||||
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 preciseAccelerationSec = (targetMs - movementMs) / 1000.0;
|
final preciseAccelerationSec = (targetMs - movementMs) / 1000.0;
|
||||||
|
|
||||||
// Physical plausibility sanity check: 0-60 >= 1.5s, 0-100 >= 3.0s
|
if (preciseAccelerationSec > 0.0) {
|
||||||
final minPlausibleSec = state.target == PerformanceLaunchTarget.sixty ? 1.5 : 3.0;
|
|
||||||
|
|
||||||
if (preciseAccelerationSec >= minPlausibleSec) {
|
|
||||||
double? reactionSec;
|
double? reactionSec;
|
||||||
double? totalFromGoSec;
|
double? totalFromGoSec;
|
||||||
|
|
||||||
|
|
@ -368,7 +317,7 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
|
||||||
telemetryConfidence: confidence,
|
telemetryConfidence: confidence,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Implausible jump -> abort as physical anomaly
|
// Non-positive time anomaly
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
state: PerformanceLaunchState.aborted,
|
state: PerformanceLaunchState.aborted,
|
||||||
abortReason: PerformanceAbortReason.physicalAnomaly,
|
abortReason: PerformanceAbortReason.physicalAnomaly,
|
||||||
|
|
@ -405,20 +354,6 @@ class PerformanceLaunchStateMachine extends StateNotifier<PerformanceLaunchAttem
|
||||||
_lastSample = sample;
|
_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 {
|
Future<void> _persistRun(PerformanceLaunchResult result, int timeMs) async {
|
||||||
try {
|
try {
|
||||||
await db.addPerformanceRun(
|
await db.addPerformanceRun(
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ void main() {
|
||||||
|
|
||||||
expect(stateMachine!.state.state, PerformanceLaunchState.completed);
|
expect(stateMachine!.state.state, PerformanceLaunchState.completed);
|
||||||
expect(stateMachine!.state.lastResult!.reactionTimeSec, isNotNull);
|
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));
|
expect(stateMachine!.state.lastResult!.totalTimeFromGoSec, greaterThan(2.5));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue