| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- /// Rolling 5 km fuel economy calculated from the ECU fuel rate or MAF.
- ///
- /// The ECU's engine-fuel-rate PID (`5E`) is preferred because it uses the same
- /// injection data as the car's trip computer and works for petrol and diesel.
- /// When that PID is unavailable, MAF and petrol stoichiometric constants
- /// (AFR 14.7, 745 g/L) are used as a fallback. The fallback 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;
- static const _maxPlausibleFuelRateLph = 200.0;
- final List<_Segment> _segments = [];
- DateTime? _lastAt;
- bool _hasPrevious = false;
- 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;
- _hasPrevious = false;
- }
- void add({
- required DateTime at,
- required double? speedKmh,
- required double? mafGps,
- required double? throttlePercent,
- double? fuelRateLph,
- }) {
- final previousAt = _lastAt;
- _lastAt = at;
- // A supported PID can briefly return a placeholder or a corrupt frame.
- // Ignore implausible values and fall back to MAF for this sample.
- final directFuelRate =
- fuelRateLph != null &&
- fuelRateLph >= 0 &&
- fuelRateLph <= _maxPlausibleFuelRateLph
- ? fuelRateLph
- : null;
- if (speedKmh == null || (directFuelRate == null && mafGps == null)) {
- _hasPrevious = false;
- return;
- }
- if (previousAt == null ||
- !_hasPrevious ||
- at.difference(previousAt) > _maxGap ||
- at.isBefore(previousAt)) {
- if (previousAt != null && at.difference(previousAt) > _maxGap) {
- reset();
- }
- _lastAt = at;
- _hasPrevious = true;
- return;
- }
- final deltaSeconds = at.difference(previousAt).inMilliseconds / 1000.0;
- if (deltaSeconds <= 0) return;
- final double litresPerHour;
- if (directFuelRate != null) {
- litresPerHour = directFuelRate;
- } else {
- final fuelCut =
- throttlePercent != null && throttlePercent < 1 && speedKmh > 10;
- 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;
- _hasPrevious = true;
- }
- 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;
- }
|