/// Rolling 5 km fuel economy calculated from MAF, speed and throttle. /// /// Petrol constants: stoichiometric AFR 14.7 and 745 g/L. This is deliberately /// an estimate; the dashboard presents it as a trend, not a measurement. class ObdEconomy { static const windowKm = 5.0; static const minimumKm = 0.5; static const _maxGap = Duration(seconds: 30); static const _afr = 14.7; static const _fuelDensityGramsPerLitre = 745.0; final List<_Segment> _segments = []; DateTime? _lastAt; double? _lastSpeedKmh; double? _lastMafGps; double? get litresPer100Km { if (_segments.isEmpty) return null; var km = 0.0; var litres = 0.0; for (final segment in _segments) { km += segment.km; litres += segment.litres; } if (km < minimumKm || km <= 0) return null; return (100 * litres / km).clamp(0.0, 99.9); } double get accumulatedKm { return _segments.fold(0, (total, segment) => total + segment.km); } void reset() { _segments.clear(); _lastAt = null; _lastSpeedKmh = null; _lastMafGps = null; } void add({ required DateTime at, required double? speedKmh, required double? mafGps, required double? throttlePercent, }) { final previousAt = _lastAt; _lastAt = at; if (speedKmh == null || mafGps == null) { _lastSpeedKmh = null; _lastMafGps = null; return; } if (previousAt == null || _lastSpeedKmh == null || _lastMafGps == null || at.difference(previousAt) > _maxGap || at.isBefore(previousAt)) { if (previousAt != null && at.difference(previousAt) > _maxGap) { reset(); } _lastAt = at; _lastSpeedKmh = speedKmh; _lastMafGps = mafGps; return; } final deltaSeconds = at.difference(previousAt).inMilliseconds / 1000.0; if (deltaSeconds <= 0) return; final fuelCut = throttlePercent != null && throttlePercent < 1 && speedKmh > 10; final litresPerHour = fuelCut ? 0.0 : (mafGps / _afr) * 3600 / _fuelDensityGramsPerLitre; final km = speedKmh * deltaSeconds / 3600; if (km > 0) { _segments.add( _Segment(km: km, litres: litresPerHour * deltaSeconds / 3600), ); _trim(); } _lastAt = at; _lastSpeedKmh = speedKmh; _lastMafGps = mafGps; } void _trim() { var km = accumulatedKm; while (km > windowKm && _segments.isNotEmpty) { km -= _segments.removeAt(0).km; } } } class _Segment { const _Segment({required this.km, required this.litres}); final double km; final double litres; }