Client reported crashes, wrong/frozen data and that the app floods the adapter MCU. See docs/FIX_2026-10-12.md for the full analysis. - Poller: strictly one request outstanding; next request only after a valid 128-byte reply or a 500 ms timeout, then a quiet period + buffer flush. Default 100 ms (10 Hz), minimum 80 ms. DTC clear/probe share the same lock. - BtNotifier: filter the adapter's default (no-ECU-data) frame instead of holding last values; EcuDataStatus + status banner; auto-reconnect on link loss, stalls or long default-only runs (rate-limited). A reconnect still in flight when the user disconnects is discarded. - S300 offsets corrected by column counting (trims, PA, ERR00-03, Eth, SW5, AF); INJ/AIN little-endian; fan = SW5 FANC only. Default-frame detection requires bytes 0-16 and 32-36. - KPro RPM/MAP2/AIN little-endian (RPM still needs a bench check). - Crash hardening: zone/Flutter/platform error handlers to an in-app log (copyable from Debug); permissions requested before listing devices. - Remove temporary debug prints; graph redraws live; polling options 100-300 ms, persisted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
2.8 KiB
Dart
100 lines
2.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
|
|
|
import '../logging/app_log.dart';
|
|
|
|
/// Wraps flutter_bluetooth_serial for SPP/RFCOMM Classic BT.
|
|
///
|
|
/// Every platform call is wrapped in a timeout and try/catch: the plugin is
|
|
/// old and some Android builds throw (or never answer) on connect/close.
|
|
class BtService {
|
|
BluetoothConnection? _connection;
|
|
StreamSubscription<Uint8List>? _inputSub;
|
|
bool _connecting = false;
|
|
|
|
final StreamController<Uint8List> _rawFrameController =
|
|
StreamController<Uint8List>.broadcast();
|
|
|
|
bool get isConnected {
|
|
try {
|
|
return _connection?.isConnected ?? false;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Stream<Uint8List> get rawStream => _rawFrameController.stream;
|
|
|
|
Future<List<BluetoothDevice>> getPairedDevices() async {
|
|
return FlutterBluetoothSerial.instance.getBondedDevices();
|
|
}
|
|
|
|
Future<void> connect(BluetoothDevice device) async {
|
|
if (_connecting) {
|
|
throw StateError('A connection attempt is already in progress');
|
|
}
|
|
_connecting = true;
|
|
try {
|
|
if (_connection != null) {
|
|
await disconnect();
|
|
}
|
|
AppLog.i('Connecting to ${device.name ?? '?'} (${device.address})');
|
|
final conn = await BluetoothConnection.toAddress(device.address)
|
|
.timeout(const Duration(seconds: 15));
|
|
_connection = conn;
|
|
_inputSub = conn.input?.listen(
|
|
(Uint8List data) {
|
|
if (!_rawFrameController.isClosed) _rawFrameController.add(data);
|
|
},
|
|
onDone: () {
|
|
AppLog.w('BT input stream closed by remote/OS');
|
|
if (!_rawFrameController.isClosed && identical(_connection, conn)) {
|
|
_rawFrameController.addError('BT connection closed');
|
|
}
|
|
},
|
|
onError: (Object e) {
|
|
AppLog.w('BT input error: $e');
|
|
if (!_rawFrameController.isClosed) _rawFrameController.addError(e);
|
|
},
|
|
cancelOnError: false,
|
|
);
|
|
AppLog.i('Connected');
|
|
} on TimeoutException {
|
|
AppLog.w('Connect timed out');
|
|
throw Exception('Connection timed out — is the adapter powered and '
|
|
'not connected to another phone?');
|
|
} finally {
|
|
_connecting = false;
|
|
}
|
|
}
|
|
|
|
Future<void> write(Uint8List data) async {
|
|
final conn = _connection;
|
|
if (conn == null || !isConnected) {
|
|
throw StateError('Not connected');
|
|
}
|
|
conn.output.add(data);
|
|
await conn.output.allSent;
|
|
}
|
|
|
|
Future<void> disconnect() async {
|
|
final conn = _connection;
|
|
_connection = null;
|
|
await _inputSub?.cancel();
|
|
_inputSub = null;
|
|
if (conn == null) return;
|
|
try {
|
|
await conn.close().timeout(const Duration(seconds: 3));
|
|
} catch (e) {
|
|
AppLog.w('BT close error (ignored): $e');
|
|
}
|
|
}
|
|
|
|
void dispose() {
|
|
disconnect();
|
|
_rawFrameController.close();
|
|
}
|
|
}
|