obd_controller.dart 6.6 KB

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