284 lines
9.2 KiB
Dart
284 lines
9.2 KiB
Dart
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<BluetoothConnectionState> connectionState =
|
|
ValueNotifier(BluetoothConnectionState.disconnected);
|
|
// MODIFIED: Changed to ValueNotifier for consistency and reactivity.
|
|
final ValueNotifier<String?> connectedDeviceName = ValueNotifier(null);
|
|
// ADDED: Notifier to hold the parsed Sonde ID.
|
|
final ValueNotifier<String?> sondeId = ValueNotifier(null);
|
|
|
|
final StreamController<Map<String, double>> _dataStreamController =
|
|
StreamController<Map<String, double>>.broadcast();
|
|
Stream<Map<String, double>> get dataStream => _dataStreamController.stream;
|
|
|
|
// --- Internal State ---
|
|
String? connectedDeviceAddress;
|
|
int _runningCounter = 0;
|
|
int _communicationLevel = 0;
|
|
String? _parentAddress;
|
|
final List<String> _parameterList = [];
|
|
|
|
Future<List<BluetoothDevice>> getPairedDevices() async {
|
|
try {
|
|
return await _bluetooth.getBondedDevices();
|
|
} catch (e) {
|
|
print("Error getting paired devices: $e");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
Future<void> 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: 2)}) {
|
|
// 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<double> 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<String, double> 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();
|
|
}
|
|
} |