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? _inputSub; bool _connecting = false; final StreamController _rawFrameController = StreamController.broadcast(); bool get isConnected { try { return _connection?.isConnected ?? false; } catch (_) { return false; } } Stream get rawStream => _rawFrameController.stream; Future> getPairedDevices() async { return FlutterBluetoothSerial.instance.getBondedDevices(); } Future 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 write(Uint8List data) async { final conn = _connection; if (conn == null || !isConnected) { throw StateError('Not connected'); } conn.output.add(data); await conn.output.allSent; } Future 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(); } }