MotorSignal-App/lib/services/simulation/telemetry_simulation_engine...

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;
}