| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- import 'dart:async';
- import 'package:flutter/foundation.dart';
- import '../../data/models/bluetooth_device_info.dart';
- import '../../data/models/obd_state.dart';
- import '../../data/repositories/settings_repository.dart';
- import '../../data/services/telemetry_bridge.dart';
- import 'obd_economy.dart';
- /// Connection lifecycle and rolling economy for the optional ELM327 adapter.
- ///
- /// Bluetooth Classic SPP only; the adapter must already be paired in Android
- /// Settings. The native side queues the read-only PID requests.
- class ObdController extends ChangeNotifier {
- ObdController(this._platform, this._settings, {DateTime Function()? clock})
- : _clock = clock ?? DateTime.now;
- final TelemetryPlatform _platform;
- final SettingsRepository _settings;
- final DateTime Function() _clock;
- final ObdEconomy _economy = ObdEconomy();
- String? _selectedAddress;
- List<BluetoothDeviceInfo> _devices = const [];
- bool _loadingDevices = false;
- String? _devicesError;
- bool _needsBluetoothPermission = false;
- ObdState _state = const ObdState();
- String? _localError;
- Timer? _pollTimer;
- bool _active = false;
- bool _polling = false;
- bool _disposed = false;
- int? _lastEconomyTimestamp;
- String? get selectedAddress => _selectedAddress;
- List<BluetoothDeviceInfo> get devices => _devices;
- bool get loadingDevices => _loadingDevices;
- String? get devicesError => _devicesError;
- bool get needsBluetoothPermission => _needsBluetoothPermission;
- bool get connected => _state.connected;
- bool get connecting => _state.connecting;
- bool get hasConfiguredAdapter => _selectedAddress != null;
- String? get error {
- if (_localError != null) return _localError;
- if (_state.connected) return null;
- return _state.error;
- }
- String get statusLabel {
- if (_loadingDevices) return 'Checking paired devices…';
- if (_needsBluetoothPermission) return 'Bluetooth permission needed';
- if (connecting) return 'Connecting…';
- if (connected) {
- return isFresh ? 'Connected' : 'Connected — waiting for data';
- }
- if (error != null) return 'Not connected — $error';
- if (_selectedAddress == null) return 'Off';
- return 'Not connected';
- }
- bool get isFresh {
- if (!_state.connected || _state.updatedAt <= 0) return false;
- final age = _clock().millisecondsSinceEpoch - _state.updatedAt;
- return age >= 0 && age < 5000;
- }
- bool get hasTelemetry =>
- isFresh && (_state.speedKmh != null || _state.throttlePercent != null);
- double? get speedKmh => isFresh ? _state.speedKmh : null;
- double? get throttlePercent => isFresh ? _state.throttlePercent : null;
- double? get ambientCelsius =>
- _state.connected && isFresh ? _state.ambientCelsius : null;
- double? get economyL100 => isFresh ? _economy.litresPer100Km : null;
- void initialize() {
- _selectedAddress = _settings.obdAdapterAddress;
- }
- Future<void> start() async {
- if (_active) return;
- _active = true;
- if (_selectedAddress != null) {
- await _connect();
- }
- _pollTimer = Timer.periodic(const Duration(seconds: 1), (_) => _poll());
- _notify();
- }
- void stop() {
- _active = false;
- _pollTimer?.cancel();
- _pollTimer = null;
- _lastEconomyTimestamp = null;
- _economy.reset();
- _state = const ObdState();
- unawaited(_platform.disconnectObd());
- _notify();
- }
- Future<void> selectAdapter(String? address) async {
- if (_selectedAddress == address) return;
- _selectedAddress = address;
- _localError = null;
- _lastEconomyTimestamp = null;
- _economy.reset();
- _state = const ObdState();
- await _settings.setObdAdapterAddress(address);
- await _platform.disconnectObd();
- if (_active && address != null) {
- await _connect();
- }
- _notify();
- }
- /// Lists bonded devices. Pass [requestPermission] only when the user asked to
- /// find adapters; the Settings card can show a dedicated allow button.
- Future<bool> refreshDevices({bool requestPermission = false}) async {
- if (_loadingDevices) return false;
- _loadingDevices = true;
- _devicesError = null;
- _notify();
- try {
- if (requestPermission) {
- final granted = await _platform.ensureBluetoothPermission();
- if (!granted) {
- _needsBluetoothPermission = true;
- _devicesError = 'Bluetooth permission was denied.';
- return false;
- }
- }
- final enabled = await _platform.isBluetoothEnabled();
- if (!enabled) {
- _needsBluetoothPermission = false;
- _devicesError = 'Bluetooth is turned off.';
- return false;
- }
- final found = await _platform.listBondedDevices();
- _devices = List.unmodifiable(found);
- _needsBluetoothPermission = false;
- return true;
- } on TelemetryException catch (problem) {
- _needsBluetoothPermission = problem.message.toLowerCase().contains(
- 'permission',
- );
- _devicesError = problem.message;
- return false;
- } finally {
- _loadingDevices = false;
- _notify();
- }
- }
- Future<void> _connect() async {
- final address = _selectedAddress;
- if (address == null) return;
- _localError = null;
- try {
- await _platform.connectObd(address);
- } on TelemetryException catch (problem) {
- _localError = problem.message;
- } catch (_) {
- _localError = 'Could not connect to the OBD adapter.';
- }
- _notify();
- }
- Future<void> _poll() async {
- if (!_active || _polling) return;
- _polling = true;
- try {
- final next = await _platform.getObdState();
- final lostConnection = _state.connected && !next.connected;
- if (lostConnection) {
- _economy.reset();
- _lastEconomyTimestamp = null;
- }
- _state = next;
- if (next.connected && next.updatedAt > 0) {
- if (_lastEconomyTimestamp != next.updatedAt) {
- _lastEconomyTimestamp = next.updatedAt;
- _economy.add(
- at: _clock(),
- speedKmh: next.speedKmh,
- mafGps: next.mafGramsPerSecond,
- throttlePercent: next.throttlePercent,
- );
- }
- }
- } catch (_) {
- // Keep the last known state; the next poll may recover.
- } finally {
- _polling = false;
- _notify();
- }
- }
- void _notify() {
- if (!_disposed) notifyListeners();
- }
- @override
- void dispose() {
- _disposed = true;
- _pollTimer?.cancel();
- unawaited(_platform.disconnectObd());
- super.dispose();
- }
- }
|