obd_controller.dart 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import '../../data/models/bluetooth_device_info.dart';
  4. import '../../data/models/obd_state.dart';
  5. import '../../data/repositories/settings_repository.dart';
  6. import '../../data/services/telemetry_bridge.dart';
  7. import 'obd_economy.dart';
  8. /// Connection lifecycle and rolling economy for the optional ELM327 adapter.
  9. ///
  10. /// Bluetooth Classic SPP only; the adapter must already be paired in Android
  11. /// Settings. The native side queues the read-only PID requests.
  12. class ObdController extends ChangeNotifier {
  13. ObdController(this._platform, this._settings, {DateTime Function()? clock})
  14. : _clock = clock ?? DateTime.now;
  15. final TelemetryPlatform _platform;
  16. final SettingsRepository _settings;
  17. final DateTime Function() _clock;
  18. final ObdEconomy _economy = ObdEconomy();
  19. String? _selectedAddress;
  20. List<BluetoothDeviceInfo> _devices = const [];
  21. bool _loadingDevices = false;
  22. String? _devicesError;
  23. bool _needsBluetoothPermission = false;
  24. ObdState _state = const ObdState();
  25. String? _localError;
  26. Timer? _pollTimer;
  27. bool _active = false;
  28. bool _polling = false;
  29. bool _disposed = false;
  30. int? _lastEconomyTimestamp;
  31. String? get selectedAddress => _selectedAddress;
  32. List<BluetoothDeviceInfo> get devices => _devices;
  33. bool get loadingDevices => _loadingDevices;
  34. String? get devicesError => _devicesError;
  35. bool get needsBluetoothPermission => _needsBluetoothPermission;
  36. bool get connected => _state.connected;
  37. bool get connecting => _state.connecting;
  38. bool get hasConfiguredAdapter => _selectedAddress != null;
  39. String? get error {
  40. if (_localError != null) return _localError;
  41. if (_state.connected) return null;
  42. return _state.error;
  43. }
  44. String get statusLabel {
  45. if (_loadingDevices) return 'Checking paired devices…';
  46. if (_needsBluetoothPermission) return 'Bluetooth permission needed';
  47. if (connecting) return 'Connecting…';
  48. if (connected) {
  49. return isFresh ? 'Connected' : 'Connected — waiting for data';
  50. }
  51. if (error != null) return 'Not connected — $error';
  52. if (_selectedAddress == null) return 'Off';
  53. return 'Not connected';
  54. }
  55. bool get isFresh {
  56. if (!_state.connected || _state.updatedAt <= 0) return false;
  57. final age = _clock().millisecondsSinceEpoch - _state.updatedAt;
  58. return age >= 0 && age < 5000;
  59. }
  60. bool get hasTelemetry =>
  61. isFresh && (_state.speedKmh != null || _state.throttlePercent != null);
  62. double? get speedKmh => isFresh ? _state.speedKmh : null;
  63. double? get throttlePercent => isFresh ? _state.throttlePercent : null;
  64. double? get ambientCelsius =>
  65. _state.connected && isFresh ? _state.ambientCelsius : null;
  66. double? get economyL100 => isFresh ? _economy.litresPer100Km : null;
  67. void initialize() {
  68. _selectedAddress = _settings.obdAdapterAddress;
  69. }
  70. Future<void> start() async {
  71. if (_active) return;
  72. _active = true;
  73. if (_selectedAddress != null) {
  74. await _connect();
  75. }
  76. _pollTimer = Timer.periodic(const Duration(seconds: 1), (_) => _poll());
  77. _notify();
  78. }
  79. void stop() {
  80. _active = false;
  81. _pollTimer?.cancel();
  82. _pollTimer = null;
  83. _lastEconomyTimestamp = null;
  84. _economy.reset();
  85. _state = const ObdState();
  86. unawaited(_platform.disconnectObd());
  87. _notify();
  88. }
  89. Future<void> selectAdapter(String? address) async {
  90. if (_selectedAddress == address) return;
  91. _selectedAddress = address;
  92. _localError = null;
  93. _lastEconomyTimestamp = null;
  94. _economy.reset();
  95. _state = const ObdState();
  96. await _settings.setObdAdapterAddress(address);
  97. await _platform.disconnectObd();
  98. if (_active && address != null) {
  99. await _connect();
  100. }
  101. _notify();
  102. }
  103. /// Lists bonded devices. Pass [requestPermission] only when the user asked to
  104. /// find adapters; the Settings card can show a dedicated allow button.
  105. Future<bool> refreshDevices({bool requestPermission = false}) async {
  106. if (_loadingDevices) return false;
  107. _loadingDevices = true;
  108. _devicesError = null;
  109. _notify();
  110. try {
  111. if (requestPermission) {
  112. final granted = await _platform.ensureBluetoothPermission();
  113. if (!granted) {
  114. _needsBluetoothPermission = true;
  115. _devicesError = 'Bluetooth permission was denied.';
  116. return false;
  117. }
  118. }
  119. final enabled = await _platform.isBluetoothEnabled();
  120. if (!enabled) {
  121. _needsBluetoothPermission = false;
  122. _devicesError = 'Bluetooth is turned off.';
  123. return false;
  124. }
  125. final found = await _platform.listBondedDevices();
  126. _devices = List.unmodifiable(found);
  127. _needsBluetoothPermission = false;
  128. return true;
  129. } on TelemetryException catch (problem) {
  130. _needsBluetoothPermission = problem.message.toLowerCase().contains(
  131. 'permission',
  132. );
  133. _devicesError = problem.message;
  134. return false;
  135. } finally {
  136. _loadingDevices = false;
  137. _notify();
  138. }
  139. }
  140. Future<void> _connect() async {
  141. final address = _selectedAddress;
  142. if (address == null) return;
  143. _localError = null;
  144. try {
  145. await _platform.connectObd(address);
  146. } on TelemetryException catch (problem) {
  147. _localError = problem.message;
  148. } catch (_) {
  149. _localError = 'Could not connect to the OBD adapter.';
  150. }
  151. _notify();
  152. }
  153. Future<void> _poll() async {
  154. if (!_active || _polling) return;
  155. _polling = true;
  156. try {
  157. final next = await _platform.getObdState();
  158. final lostConnection = _state.connected && !next.connected;
  159. if (lostConnection) {
  160. _economy.reset();
  161. _lastEconomyTimestamp = null;
  162. }
  163. _state = next;
  164. if (next.connected && next.updatedAt > 0) {
  165. if (_lastEconomyTimestamp != next.updatedAt) {
  166. _lastEconomyTimestamp = next.updatedAt;
  167. _economy.add(
  168. at: _clock(),
  169. speedKmh: next.speedKmh,
  170. mafGps: next.mafGramsPerSecond,
  171. throttlePercent: next.throttlePercent,
  172. );
  173. }
  174. }
  175. } catch (_) {
  176. // Keep the last known state; the next poll may recover.
  177. } finally {
  178. _polling = false;
  179. _notify();
  180. }
  181. }
  182. void _notify() {
  183. if (!_disposed) notifyListeners();
  184. }
  185. @override
  186. void dispose() {
  187. _disposed = true;
  188. _pollTimer?.cancel();
  189. unawaited(_platform.disconnectObd());
  190. super.dispose();
  191. }
  192. }