209 lines
6.6 KiB
Dart
209 lines
6.6 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! >= -2000 &&
|
|
gpsAgeMs! <= RecordingHealthContract.evidenceStaleAfterMs;
|
|
}
|
|
if (gpsTimestamp != null) {
|
|
final age = timestamp.difference(gpsTimestamp!).inMilliseconds;
|
|
return age >= -2000 &&
|
|
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! >= -2000 &&
|
|
gpsAgeMs! <= RecordingHealthContract.evidenceStaleAfterMs;
|
|
}
|
|
if (gpsTimestamp != null) {
|
|
final age = timestamp.difference(gpsTimestamp!).inMilliseconds;
|
|
return age >= -2000 &&
|
|
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,
|
|
);
|
|
}
|
|
}
|