obd_economy.dart 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. /// Rolling 5 km fuel economy calculated from the ECU fuel rate or MAF.
  2. ///
  3. /// The ECU's engine-fuel-rate PID (`5E`) is preferred because it uses the same
  4. /// injection data as the car's trip computer and works for petrol and diesel.
  5. /// When that PID is unavailable, MAF and petrol stoichiometric constants
  6. /// (AFR 14.7, 745 g/L) are used as a fallback. The fallback is deliberately an
  7. /// estimate; the dashboard presents it as a trend, not a measurement.
  8. class ObdEconomy {
  9. static const windowKm = 5.0;
  10. static const minimumKm = 0.5;
  11. static const _maxGap = Duration(seconds: 30);
  12. static const _afr = 14.7;
  13. static const _fuelDensityGramsPerLitre = 745.0;
  14. static const _maxPlausibleFuelRateLph = 200.0;
  15. final List<_Segment> _segments = [];
  16. DateTime? _lastAt;
  17. bool _hasPrevious = false;
  18. double? get litresPer100Km {
  19. if (_segments.isEmpty) return null;
  20. var km = 0.0;
  21. var litres = 0.0;
  22. for (final segment in _segments) {
  23. km += segment.km;
  24. litres += segment.litres;
  25. }
  26. if (km < minimumKm || km <= 0) return null;
  27. return (100 * litres / km).clamp(0.0, 99.9);
  28. }
  29. double get accumulatedKm {
  30. return _segments.fold(0, (total, segment) => total + segment.km);
  31. }
  32. void reset() {
  33. _segments.clear();
  34. _lastAt = null;
  35. _hasPrevious = false;
  36. }
  37. void add({
  38. required DateTime at,
  39. required double? speedKmh,
  40. required double? mafGps,
  41. required double? throttlePercent,
  42. double? fuelRateLph,
  43. }) {
  44. final previousAt = _lastAt;
  45. _lastAt = at;
  46. // A supported PID can briefly return a placeholder or a corrupt frame.
  47. // Ignore implausible values and fall back to MAF for this sample.
  48. final directFuelRate =
  49. fuelRateLph != null &&
  50. fuelRateLph >= 0 &&
  51. fuelRateLph <= _maxPlausibleFuelRateLph
  52. ? fuelRateLph
  53. : null;
  54. if (speedKmh == null || (directFuelRate == null && mafGps == null)) {
  55. _hasPrevious = false;
  56. return;
  57. }
  58. if (previousAt == null ||
  59. !_hasPrevious ||
  60. at.difference(previousAt) > _maxGap ||
  61. at.isBefore(previousAt)) {
  62. if (previousAt != null && at.difference(previousAt) > _maxGap) {
  63. reset();
  64. }
  65. _lastAt = at;
  66. _hasPrevious = true;
  67. return;
  68. }
  69. final deltaSeconds = at.difference(previousAt).inMilliseconds / 1000.0;
  70. if (deltaSeconds <= 0) return;
  71. final double litresPerHour;
  72. if (directFuelRate != null) {
  73. litresPerHour = directFuelRate;
  74. } else {
  75. final fuelCut =
  76. throttlePercent != null && throttlePercent < 1 && speedKmh > 10;
  77. litresPerHour = fuelCut
  78. ? 0.0
  79. : (mafGps! / _afr) * 3600 / _fuelDensityGramsPerLitre;
  80. }
  81. final km = speedKmh * deltaSeconds / 3600;
  82. if (km > 0) {
  83. _segments.add(
  84. _Segment(km: km, litres: litresPerHour * deltaSeconds / 3600),
  85. );
  86. _trim();
  87. }
  88. _lastAt = at;
  89. _hasPrevious = true;
  90. }
  91. void _trim() {
  92. var km = accumulatedKm;
  93. while (km > windowKm && _segments.isNotEmpty) {
  94. km -= _segments.removeAt(0).km;
  95. }
  96. }
  97. }
  98. class _Segment {
  99. const _Segment({required this.km, required this.litres});
  100. final double km;
  101. final double litres;
  102. }