470 lines
15 KiB
Dart
470 lines
15 KiB
Dart
import 'dart:math' as math;
|
|
import '../../models/telemetry_sample.dart';
|
|
import '../validation/telemetry_validator.dart';
|
|
|
|
enum SimulationScenarioType {
|
|
stationaryMotorcycle,
|
|
smoothAcceleration,
|
|
zeroTo60Run,
|
|
zeroTo100Run,
|
|
hardBraking,
|
|
smoothLeftCorner,
|
|
smoothRightCorner,
|
|
highGAcceleration,
|
|
highGBraking,
|
|
gpsDropout,
|
|
gpsAccuracyDegradation,
|
|
gpsPositionJump,
|
|
sensorDropout,
|
|
duplicateTimestamp,
|
|
timestampRegression,
|
|
irregular20HzTiming,
|
|
appBackgroundingRecovery,
|
|
storageBackpressure,
|
|
}
|
|
|
|
class TelemetrySimulationEngine {
|
|
/// Generates synthetic 20Hz sample streams for all 18 test scenarios
|
|
List<TelemetrySample> generateScenario(SimulationScenarioType scenario, {DateTime? baseTime}) {
|
|
final t0 = baseTime ?? DateTime(2026, 8, 29, 12, 0, 0);
|
|
final List<TelemetrySample> samples = [];
|
|
|
|
switch (scenario) {
|
|
case SimulationScenarioType.stationaryMotorcycle:
|
|
// 5 seconds at 0 MPH with engine idle vibration (100 samples)
|
|
for (int i = 0; i < 100; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final vib = math.sin(i * 0.8) * 0.4;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 0.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.5,
|
|
leanAngle: vib,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
accelX: 0.0,
|
|
accelY: 0.0,
|
|
accelZ: 9.81,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.smoothAcceleration:
|
|
// 0 to 60 MPH over 5.0 seconds (100 samples)
|
|
for (int i = 0; i < 100; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final progress = i / 100.0;
|
|
final speed = progress * 60.0;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: speed,
|
|
latitude: 34.0000 + (progress * 0.001),
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.2,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 2.0,
|
|
longitudinalG: 0.55,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.zeroTo60Run:
|
|
// 0 to 60 MPH sport launch in 3.2 seconds (64 samples)
|
|
for (int i = 0; i < 64; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final progress = i / 64.0;
|
|
final speed = progress * 60.0;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: speed,
|
|
latitude: 34.0000 + (progress * 0.0008),
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 4.5,
|
|
longitudinalG: 0.85,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.zeroTo100Run:
|
|
// 0 to 100 MPH high speed sprint in 7.5 seconds (150 samples)
|
|
for (int i = 0; i < 150; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final progress = i / 150.0;
|
|
final speed = progress * 100.0;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: speed,
|
|
latitude: 34.0000 + (progress * 0.003),
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 3.0,
|
|
longitudinalG: 0.70,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.hardBraking:
|
|
// 70 to 0 MPH in 2.8 seconds under 1.15G deceleration (56 samples)
|
|
for (int i = 0; i < 56; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final progress = i / 56.0;
|
|
final speed = (1.0 - progress) * 70.0;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: speed,
|
|
latitude: 34.0000 + (progress * 0.0005),
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: -5.0,
|
|
longitudinalG: -1.15,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.smoothLeftCorner:
|
|
// 45 MPH sustained 35 deg left lean for 4.0 seconds (80 samples)
|
|
for (int i = 0; i < 80; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 45.0,
|
|
latitude: 34.0000 + (i * 0.00002),
|
|
longitude: -85.0000 + (i * 0.00002),
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: -35.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.70,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.smoothRightCorner:
|
|
// 50 MPH sustained 25 deg right lean for 3.5 seconds (70 samples)
|
|
for (int i = 0; i < 70; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 50.0,
|
|
latitude: 34.0000 + (i * 0.00002),
|
|
longitude: -85.0000 - (i * 0.00002),
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 25.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: -0.46,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.highGAcceleration:
|
|
// Snap acceleration at 0.8G
|
|
for (int i = 0; i < 40; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 30.0 + (i * 1.0),
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 4.0,
|
|
longitudinalG: 0.80,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.highGBraking:
|
|
// Emergency stop at 1.4G
|
|
for (int i = 0; i < 40; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: math.max(0.0, 60.0 - (i * 1.5)),
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: -6.0,
|
|
longitudinalG: -1.40,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.gpsDropout:
|
|
// 5 seconds nominal GPS, 5 seconds null GPS (tunnel), 5 seconds restored
|
|
for (int i = 0; i < 300; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final inTunnel = i >= 100 && i < 200;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: inTunnel ? null : 65.0,
|
|
latitude: inTunnel ? null : 34.0000 + (i * 0.0001),
|
|
longitude: inTunnel ? null : -85.0000,
|
|
gpsAccuracy: inTunnel ? null : 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.gpsAccuracyDegradation:
|
|
// GPS accuracy degrades from 3m to 85m
|
|
for (int i = 0; i < 100; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final acc = 3.0 + (i * 0.82); // goes up to 85m
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 55.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: acc,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.gpsPositionJump:
|
|
// Normal motion with an impossible 200m teleportation jump in frame 50
|
|
for (int i = 0; i < 100; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final lat = i == 50 ? 34.0020 : 34.0000 + (i * 0.00001); // 200m glitch
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 45.0,
|
|
latitude: lat,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.sensorDropout:
|
|
// IMU dropout (null accel/lean)
|
|
for (int i = 0; i < 100; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
final isImuNull = i >= 40 && i < 60;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 50.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: isImuNull ? null : 5.0,
|
|
pitchAngle: isImuNull ? null : 0.0,
|
|
longitudinalG: isImuNull ? null : 0.0,
|
|
lateralG: isImuNull ? null : 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.duplicateTimestamp:
|
|
// Ingest duplicate timestamp in middle
|
|
for (int i = 0; i < 50; i++) {
|
|
final isDup = i == 25;
|
|
final t = isDup ? t0.add(Duration(milliseconds: 24 * 50)) : t0.add(Duration(milliseconds: i * 50));
|
|
final monoMs = isDup ? 24 * 50 : i * 50;
|
|
final seq = isDup ? 25 : i + 1;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: seq,
|
|
monotonicTimestampMs: monoMs,
|
|
speed: 40.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.timestampRegression:
|
|
// Ingest backward timestamp in middle
|
|
for (int i = 0; i < 50; i++) {
|
|
final isRegress = i == 25;
|
|
final t = isRegress ? t0.add(Duration(milliseconds: 20 * 50)) : t0.add(Duration(milliseconds: i * 50));
|
|
final monoMs = isRegress ? 20 * 50 : i * 50;
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: monoMs,
|
|
speed: 40.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.irregular20HzTiming:
|
|
// Ingest jittery timing intervals (20ms, 120ms, 40ms)
|
|
int currentMs = 0;
|
|
for (int i = 0; i < 50; i++) {
|
|
final step = (i % 3 == 0) ? 120 : ((i % 3 == 1) ? 20 : 50);
|
|
currentMs += step;
|
|
final t = t0.add(Duration(milliseconds: currentMs));
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: currentMs,
|
|
speed: 40.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.appBackgroundingRecovery:
|
|
// Normal ride interrupted by 30s backgrounding gap
|
|
for (int i = 0; i < 100; i++) {
|
|
final offsetMs = i < 50 ? i * 50 : (50 * 50) + 30000 + ((i - 50) * 50);
|
|
final t = t0.add(Duration(milliseconds: offsetMs));
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: offsetMs,
|
|
speed: 50.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case SimulationScenarioType.storageBackpressure:
|
|
// Large volume burst (500 samples)
|
|
for (int i = 0; i < 500; i++) {
|
|
final t = t0.add(Duration(milliseconds: i * 50));
|
|
samples.add(TelemetrySample(
|
|
timestamp: t,
|
|
sequenceNumber: i + 1,
|
|
monotonicTimestampMs: i * 50,
|
|
speed: 55.0,
|
|
latitude: 34.0000,
|
|
longitude: -85.0000,
|
|
gpsAccuracy: 3.0,
|
|
leanAngle: 0.0,
|
|
pitchAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
));
|
|
}
|
|
break;
|
|
}
|
|
|
|
return samples;
|
|
}
|
|
|
|
/// Replays a list of TelemetrySamples through validation and evaluates results
|
|
SimulationResult runSimulation(List<TelemetrySample> samples) {
|
|
final validator = TelemetryValidator();
|
|
int validCount = 0;
|
|
int suspectCount = 0;
|
|
int rejectedCount = 0;
|
|
final List<String> issuesLogged = [];
|
|
|
|
for (final s in samples) {
|
|
final q = validator.validate(s);
|
|
if (q.isValid) {
|
|
validCount++;
|
|
} else if (q.isSuspect) {
|
|
suspectCount++;
|
|
issuesLogged.addAll(q.reasons);
|
|
} else {
|
|
rejectedCount++;
|
|
issuesLogged.addAll(q.reasons);
|
|
}
|
|
}
|
|
|
|
return SimulationResult(
|
|
totalSamples: samples.length,
|
|
validSamples: validCount,
|
|
suspectSamples: suspectCount,
|
|
rejectedSamples: rejectedCount,
|
|
issues: issuesLogged,
|
|
);
|
|
}
|
|
}
|
|
|
|
class SimulationResult {
|
|
final int totalSamples;
|
|
final int validSamples;
|
|
final int suspectSamples;
|
|
final int rejectedSamples;
|
|
final List<String> issues;
|
|
|
|
const SimulationResult({
|
|
required this.totalSamples,
|
|
required this.validSamples,
|
|
required this.suspectSamples,
|
|
required this.rejectedSamples,
|
|
required this.issues,
|
|
});
|
|
|
|
double get validRatio => totalSamples > 0 ? validSamples / totalSamples : 0.0;
|
|
}
|