MotorSignal-App/lib/models/telemetry_sample.dart

209 lines
6.5 KiB
Dart

import 'recording_health_status.dart';
import '../services/validation/telemetry_validator.dart';
class TelemetrySample {
final DateTime timestamp;
final int? sequenceNumber;
final int? monotonicTimestampMs;
// Location Data
final double? latitude;
final double? longitude;
final double? altitude;
final double? speed; // MPH
final double? heading;
final double? gpsAccuracy;
final DateTime? gpsTimestamp;
final int? gpsAgeMs;
// IMU Data
final DateTime? imuTimestamp;
final double? leanAngle;
final double? pitchAngle;
final double? rollRateDegPerSec;
final double? accelX;
final double? accelY;
final double? accelZ;
final double? longitudinalG;
final double? lateralG;
final double? gyroX;
final double? gyroY;
final double? gyroZ;
// Raw Preserved Sensors (Unfiltered for forensic analysis)
final double? rawAccelX;
final double? rawAccelY;
final double? rawAccelZ;
final double? rawGyroX;
final double? rawGyroY;
final double? rawGyroZ;
// Validation & Quality
final TelemetryQuality? quality;
TelemetrySample({
required this.timestamp,
this.sequenceNumber,
this.monotonicTimestampMs,
this.latitude,
this.longitude,
this.altitude,
this.speed,
this.heading,
this.gpsAccuracy,
this.gpsTimestamp,
this.gpsAgeMs,
this.imuTimestamp,
this.leanAngle,
this.pitchAngle,
this.rollRateDegPerSec,
this.accelX,
this.accelY,
this.accelZ,
this.longitudinalG,
this.lateralG,
this.gyroX,
this.gyroY,
this.gyroZ,
this.rawAccelX,
this.rawAccelY,
this.rawAccelZ,
this.rawGyroX,
this.rawGyroY,
this.rawGyroZ,
this.quality,
});
/// Whether the encapsulated speed is fresh according to the canonical
/// evidence staleness bound (4000 ms).
///
/// Returns false when:
/// - [speed] is null (no speed measurement available)
/// - [gpsAgeMs] exceeds [RecordingHealthContract.evidenceStaleAfterMs] (4000 ms)
/// - [gpsTimestamp] age exceeds [RecordingHealthContract.evidenceStaleAfterMs] relative to [timestamp]
///
/// Returns true for:
/// - Genuinely advancing GPS fixes with valid speed
/// - Intermediate 10 Hz / 20 Hz sample repetitions within the 4000 ms bound
/// - Fresh 0.0 MPH stopped fixes
/// - Synthetic test samples where GPS metadata was not explicitly attached
bool get isSpeedFresh {
if (speed == null) return false;
if (gpsAgeMs != null) {
return gpsAgeMs! >= 0 &&
gpsAgeMs! <= RecordingHealthContract.evidenceStaleAfterMs;
}
if (gpsTimestamp != null) {
final age = timestamp.difference(gpsTimestamp!).inMilliseconds;
return age >= 0 &&
age <= RecordingHealthContract.evidenceStaleAfterMs;
}
// Synthetic test samples without GPS age/timestamp retain speed validity.
return true;
}
/// Whether the encapsulated GNSS evidence (coordinates, heading, accuracy) is
/// fresh according to the canonical evidence staleness bound (4000 ms).
///
/// Returns false when:
/// - [latitude] or [longitude] is absent, non-finite, or out of range
/// - [gpsAgeMs] exceeds [RecordingHealthContract.evidenceStaleAfterMs] (4000 ms)
/// - [gpsTimestamp] age exceeds [RecordingHealthContract.evidenceStaleAfterMs] relative to [timestamp]
///
/// Returns true for:
/// - Genuinely advancing GPS fixes with valid coordinates
/// - Intermediate 10 Hz / 20 Hz sample repetitions within the 4000 ms bound
/// - Synthetic test samples where GPS metadata was not explicitly attached
bool get isGpsFresh {
if (latitude == null || longitude == null ||
!latitude!.isFinite || !longitude!.isFinite ||
latitude!.abs() > 90 || longitude!.abs() > 180) {
return false;
}
if (gpsAgeMs != null) {
return gpsAgeMs! >= 0 &&
gpsAgeMs! <= RecordingHealthContract.evidenceStaleAfterMs;
}
if (gpsTimestamp != null) {
final age = timestamp.difference(gpsTimestamp!).inMilliseconds;
return age >= 0 &&
age <= RecordingHealthContract.evidenceStaleAfterMs;
}
// Synthetic test samples without GPS age/timestamp retain validity.
return true;
}
/// Current trustworthy speed in MPH, or null if GPS evidence is absent or stale.
///
/// Prevents stale cached GNSS fixes from presenting old speeds indefinitely
/// while keeping genuine fresh 0.0 MPH measurements distinguishable from
/// unavailable/stale speed.
double? get freshSpeed => isSpeedFresh ? speed : null;
TelemetrySample copyWith({
DateTime? timestamp,
int? sequenceNumber,
int? monotonicTimestampMs,
double? latitude,
double? longitude,
double? altitude,
double? speed,
double? heading,
double? gpsAccuracy,
DateTime? gpsTimestamp,
int? gpsAgeMs,
DateTime? imuTimestamp,
double? leanAngle,
double? pitchAngle,
double? rollRateDegPerSec,
double? accelX,
double? accelY,
double? accelZ,
double? longitudinalG,
double? lateralG,
double? gyroX,
double? gyroY,
double? gyroZ,
double? rawAccelX,
double? rawAccelY,
double? rawAccelZ,
double? rawGyroX,
double? rawGyroY,
double? rawGyroZ,
TelemetryQuality? quality,
}) {
return TelemetrySample(
timestamp: timestamp ?? this.timestamp,
sequenceNumber: sequenceNumber ?? this.sequenceNumber,
monotonicTimestampMs: monotonicTimestampMs ?? this.monotonicTimestampMs,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
altitude: altitude ?? this.altitude,
speed: speed ?? this.speed,
heading: heading ?? this.heading,
gpsAccuracy: gpsAccuracy ?? this.gpsAccuracy,
gpsTimestamp: gpsTimestamp ?? this.gpsTimestamp,
gpsAgeMs: gpsAgeMs ?? this.gpsAgeMs,
imuTimestamp: imuTimestamp ?? this.imuTimestamp,
leanAngle: leanAngle ?? this.leanAngle,
pitchAngle: pitchAngle ?? this.pitchAngle,
rollRateDegPerSec: rollRateDegPerSec ?? this.rollRateDegPerSec,
accelX: accelX ?? this.accelX,
accelY: accelY ?? this.accelY,
accelZ: accelZ ?? this.accelZ,
longitudinalG: longitudinalG ?? this.longitudinalG,
lateralG: lateralG ?? this.lateralG,
gyroX: gyroX ?? this.gyroX,
gyroY: gyroY ?? this.gyroY,
gyroZ: gyroZ ?? this.gyroZ,
rawAccelX: rawAccelX ?? this.rawAccelX,
rawAccelY: rawAccelY ?? this.rawAccelY,
rawAccelZ: rawAccelZ ?? this.rawAccelZ,
rawGyroX: rawGyroX ?? this.rawGyroX,
rawGyroY: rawGyroY ?? this.rawGyroY,
rawGyroZ: rawGyroZ ?? this.rawGyroZ,
quality: quality ?? this.quality,
);
}
}