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 generateScenario(SimulationScenarioType scenario, {DateTime? baseTime}) { final t0 = baseTime ?? DateTime(2026, 8, 29, 12, 0, 0); final List 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 samples) { final validator = TelemetryValidator(); int validCount = 0; int suspectCount = 0; int rejectedCount = 0; final List 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 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; }