import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; //import 'utils/converter.dart'; import '../serial/utils/converter.dart'; import 'utils/crc_calculator.dart'; import 'utils/parameter_helper.dart'; enum BluetoothConnectionState { disconnected, connecting, connected } class BluetoothManager { Timer? _dataRequestTimer; final FlutterBluetoothSerial _bluetooth = FlutterBluetoothSerial.instance; BluetoothConnection? _connection; // --- State Notifiers --- final ValueNotifier connectionState = ValueNotifier(BluetoothConnectionState.disconnected); // MODIFIED: Changed to ValueNotifier for consistency and reactivity. final ValueNotifier connectedDeviceName = ValueNotifier(null); // ADDED: Notifier to hold the parsed Sonde ID. final ValueNotifier sondeId = ValueNotifier(null); final StreamController> _dataStreamController = StreamController>.broadcast(); Stream> get dataStream => _dataStreamController.stream; // --- Internal State --- String? connectedDeviceAddress; int _runningCounter = 0; int _communicationLevel = 0; String? _parentAddress; final List _parameterList = []; Future> getPairedDevices() async { try { return await _bluetooth.getBondedDevices(); } catch (e) { print("Error getting paired devices: $e"); return []; } } Future connect(BluetoothDevice device) async { if (connectionState.value == BluetoothConnectionState.connected) return; try { connectionState.value = BluetoothConnectionState.connecting; _connection = await BluetoothConnection.toAddress(device.address); // MODIFIED: Set the .value of the notifier. connectedDeviceName.value = device.name; connectedDeviceAddress = device.address; connectionState.value = BluetoothConnectionState.connected; _connection!.input!.listen(_onDataReceived).onDone(disconnect); } catch (e) { print("Error connecting: $e"); disconnect(); } } void disconnect() { if (connectionState.value != BluetoothConnectionState.disconnected) { stopAutoReading(); // Ensure timer is stopped on disconnect _connection?.dispose(); _connection = null; // MODIFIED: Reset the .value of the notifiers. connectedDeviceName.value = null; // ADDED: Reset Sonde ID on disconnect. sondeId.value = null; connectedDeviceAddress = null; connectionState.value = BluetoothConnectionState.disconnected; } } /// Starts a periodic timer that requests data automatically. void startAutoReading({Duration interval = const Duration(seconds: 5)}) { // Cancel any existing timer to prevent duplicates. stopAutoReading(); // Request the first reading immediately without waiting for the timer. if (connectionState.value == BluetoothConnectionState.connected) { startLiveReading(); } // Start a new timer that calls startLiveReading periodically. _dataRequestTimer = Timer.periodic(interval, (Timer t) { if (connectionState.value == BluetoothConnectionState.connected) { startLiveReading(); } else { // If we get disconnected for any reason, stop the timer. stopAutoReading(); } }); } /// Stops the automatic data refresh timer. void stopAutoReading() { _dataRequestTimer?.cancel(); _dataRequestTimer = null; } void startLiveReading() { if (connectionState.value != BluetoothConnectionState.connected) return; _communicationLevel = 0; _parameterList.clear(); _parentAddress = null; _sendCommand(0); } void _onDataReceived(Uint8List data) { if (data.isEmpty) return; String responseHex = Converter.byteArrayToHexString(data).toUpperCase(); print("Received (Lvl: $_communicationLevel): $responseHex"); switch (_communicationLevel) { case 0: _handleResponseLevel0(responseHex); break; case 1: _handleResponseLevel1(responseHex); break; case 2: _handleResponseLevel2(responseHex); break; } } void _handleResponseLevel0(String responseHex) { try { if (responseHex.length < 94) { print("Level 0 response is too short."); return; } // ADDED: Parse Sonde ID (Serial Number) from the response. // This uses the same logic as your SerialManager. String serialHex = responseHex.substring(68, 86); String serialAscii = Converter.hexToAscii(serialHex); sondeId.value = serialAscii; print("Successfully Parsed Sonde ID: ${sondeId.value}"); // Your existing logic to parse the Parent Address. final int dataBlockLength = int.parse(responseHex.substring(30, 34), radix: 16); if (dataBlockLength == 38) { const int dataBlockStart = 34; const int parentAddressOffset = 52; final int addressStart = dataBlockStart + parentAddressOffset; _parentAddress = responseHex.substring(addressStart, addressStart + 8); print("Successfully Parsed Parent Address: $_parentAddress"); if (_parentAddress != "00000000") { _communicationLevel = 1; // Give the device a moment before we send the next command. Future.delayed(const Duration(milliseconds: 500)).then((_) { _sendCommand(1); // Move to next step }); } } } catch (e) { print("Error parsing Level 0 response: $e"); disconnect(); } } void _handleResponseLevel1(String responseHex) { try { if (responseHex.length < 38) return; final int dataBlockLength = int.parse(responseHex.substring(30, 34), radix: 16); final String parametersDataBlock = responseHex.substring(34, 34 + (dataBlockLength * 2)); _parameterList.clear(); for (int i = 0; i <= parametersDataBlock.length - 6; i += 6) { String parameterCode = parametersDataBlock.substring(i + 2, i + 6); _parameterList.add(ParameterHelper.getDescription(parameterCode)); } print("Parsed Parameters: $_parameterList"); _communicationLevel = 2; // **** UPDATED CODE **** // Give the device a moment before requesting the final data. Future.delayed(const Duration(milliseconds: 500)).then((_) { _sendCommand(2); // Move to final step }); } catch (e) { print("Error parsing Level 1 response: $e"); disconnect(); } } void _handleResponseLevel2(String responseHex) { try { if (responseHex.length < 38) return; final int dataBlockLength = int.parse(responseHex.substring(30, 34), radix: 16); final String valuesDataBlock = responseHex.substring(34, 34 + (dataBlockLength * 2)); final List parameterValues = []; for (int i = 0; i <= valuesDataBlock.length - 8; i += 8) { String valueHex = valuesDataBlock.substring(i, i + 8); parameterValues.add(Converter.hexToFloat(valueHex)); } if (_parameterList.length == parameterValues.length) { Map finalReadings = {}; for (int i = 0; i < _parameterList.length; i++) { finalReadings[_parameterList[i]] = parameterValues[i]; } print("Final Parsed Readings: $finalReadings"); _dataStreamController.add(finalReadings); } // Reset for the next reading sequence _communicationLevel = 0; } catch (e) { print("Error parsing Level 2 response: $e"); disconnect(); } } void _sendCommand(int level) { if (_connection == null || !_connection!.isConnected) { print("Cannot send command, not connected."); return; } String commandHex; if (level == 0) { commandHex = _getCommand0(); } else if (level == 1) { commandHex = _getCommand1(); } else { commandHex = _getCommand2(); } Uint8List commandBytes = Converter.hexStringToByteArray(commandHex); String crcHexString = computeCrc16Ccitt(commandBytes); String finalHexPacket = commandHex + crcHexString; Uint8List packetToSend = Converter.hexStringToByteArray(finalHexPacket); try { _connection?.output.add(packetToSend); _connection?.output.allSent.then((_) { print("Sent (Lvl: $level): $finalHexPacket"); }); } catch (e) { print("Error sending data: $e"); disconnect(); } } String _getCommand0() { String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0'); return '7E02${seqNo}0000000002000000200000010000'; } String _getCommand1() { String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0'); return '7E02$seqNo${_parentAddress}02000000200000180000'; } String _getCommand2() { String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0'); return '7E02$seqNo${_parentAddress}02000000200000190000'; } void dispose() { disconnect(); _dataStreamController.close(); connectionState.dispose(); // ADDED: Dispose of the new notifiers to prevent memory leaks. connectedDeviceName.dispose(); sondeId.dispose(); } }