544 lines
26 KiB
Dart
544 lines
26 KiB
Dart
// lib/serial/serial_manager.dart
|
|
|
|
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
import 'package:flutter/foundation.dart'; // For debugPrint
|
|
import 'package:usb_serial/usb_serial.dart';
|
|
|
|
import 'utils/converter.dart';
|
|
import 'utils/crc_calculator.dart';
|
|
import 'utils/parameter_helper.dart';
|
|
|
|
enum SerialConnectionState { disconnected, connecting, connected }
|
|
|
|
/// Manages the connection and data communication with a USB Serial device.
|
|
class SerialManager {
|
|
UsbPort? _port;
|
|
// Using ValueNotifier to easily notify UI about connection state changes.
|
|
final ValueNotifier<SerialConnectionState> connectionState = ValueNotifier(
|
|
SerialConnectionState.disconnected,
|
|
);
|
|
// ValueNotifier to expose the connected device's name.
|
|
final ValueNotifier<String?> connectedDeviceName = ValueNotifier(null);
|
|
|
|
// --- ADDED: ValueNotifier for Sonde ID ---
|
|
// This will be updated when the Sonde ID is parsed from Level 0 data.
|
|
final ValueNotifier<String?> sondeId = ValueNotifier<String?>(null);
|
|
|
|
// --- ADDED: Flag to prevent updates after disposal ---
|
|
bool _isDisposed = false;
|
|
|
|
// StreamController to broadcast parsed data readings to multiple listeners.
|
|
final StreamController<Map<String, double>> _dataStreamController =
|
|
StreamController<Map<String, double>>.broadcast();
|
|
Stream<Map<String, double>> get dataStream => _dataStreamController.stream;
|
|
|
|
// --- Protocol State Variables ---
|
|
int _runningCounter = 0; // Sequence number for commands
|
|
int _communicationLevel = 0; // Tracks the current step in the 3-level protocol
|
|
String? _parentAddress; // Address parsed from Level 0 response
|
|
String? _serialNumber; // Serial number parsed from Level 0 response (used internally for now)
|
|
List<String> _parameterList = []; // List of parameters parsed from Level 1 response
|
|
|
|
Timer? _dataRequestTimer; // Timer for periodic auto-reading
|
|
Timer? _responseTimeoutTimer; // Timer to detect if a response is not received in time
|
|
|
|
// Buffer to accumulate incoming serial data, as messages may arrive in chunks.
|
|
final StringBuffer _responseBuffer = StringBuffer();
|
|
// String to look for at the start of an expected response (e.g., '7E02 + seqNo')
|
|
String? _lookupString;
|
|
// Flag to prevent sending new commands before the current read cycle is complete.
|
|
bool _isReading = false;
|
|
|
|
/// Fetches a list of available USB devices connected to the Android device.
|
|
Future<List<UsbDevice>> getAvailableDevices() async {
|
|
return UsbSerial.listDevices();
|
|
}
|
|
|
|
/// Connects to the specified USB device.
|
|
/// Handles opening the port, setting parameters, and starting the input stream listener.
|
|
Future<void> connect(UsbDevice device) async {
|
|
// Prevent connecting if already connected
|
|
if (connectionState.value == SerialConnectionState.connected) {
|
|
debugPrint("SerialManager: Already connected. Disconnecting existing connection.");
|
|
// Now that disconnect() is async and returns Future<void>, awaiting it is correct.
|
|
await disconnect(); // Disconnect cleanly before new connection
|
|
}
|
|
|
|
try {
|
|
_isDisposed = false; // Reset disposed flag on new connection
|
|
connectionState.value = SerialConnectionState.connecting;
|
|
connectedDeviceName.value = device.productName ?? 'Unknown Device'; // Update device name early
|
|
|
|
_port = await device.create();
|
|
if (_port == null) {
|
|
debugPrint("SerialManager: Failed to create USB serial port for device: ${device.productName}.");
|
|
// No need to call disconnect, as nothing was connected yet.
|
|
connectionState.value = SerialConnectionState.disconnected; // Reset state
|
|
connectedDeviceName.value = null; // Clear device name
|
|
throw Exception("Failed to create USB serial port.");
|
|
}
|
|
|
|
bool openResult = await _port!.open();
|
|
if (!openResult) {
|
|
debugPrint("SerialManager: Failed to open USB serial port for device: ${device.productName}.");
|
|
_port = null; // Clear port reference
|
|
connectionState.value = SerialConnectionState.disconnected; // Reset state
|
|
connectedDeviceName.value = null; // Clear device name
|
|
throw Exception("Failed to open USB serial port.");
|
|
}
|
|
|
|
await _port!.setDTR(true);
|
|
await _port!.setRTS(true);
|
|
|
|
_port!.setPortParameters(
|
|
115200, // Baud Rate
|
|
UsbPort.DATABITS_8,
|
|
UsbPort.STOPBITS_1,
|
|
UsbPort.PARITY_NONE,
|
|
);
|
|
|
|
// Listen for incoming data from the USB serial port.
|
|
_port!.inputStream!.listen(
|
|
_onDataReceived,
|
|
onError: (e) {
|
|
debugPrint("SerialManager: Stream Error: $e");
|
|
// Since disconnect() is now async, calling it directly here without await is fine
|
|
// because we don't need to wait for it within the error handler.
|
|
disconnect();
|
|
},
|
|
onDone: () {
|
|
debugPrint("SerialManager: Stream Closed");
|
|
// Same here, no need to await within onDone.
|
|
disconnect();
|
|
},
|
|
);
|
|
|
|
// --- REVISED: Added null check before updating state ---
|
|
if (!_isDisposed) {
|
|
connectionState.value = SerialConnectionState.connected;
|
|
connectedDeviceName.value = device.productName;
|
|
}
|
|
debugPrint("SerialManager: Connected to ${connectedDeviceName.value}");
|
|
|
|
// Start the live reading cycle immediately after connecting
|
|
startLiveReading();
|
|
|
|
} catch (e) {
|
|
debugPrint("SerialManager: Connection Error: $e");
|
|
// Ensure all states are reset on error during connection
|
|
if (!_isDisposed) {
|
|
connectionState.value = SerialConnectionState.disconnected;
|
|
connectedDeviceName.value = null;
|
|
sondeId.value = null; // Clear Sonde ID
|
|
}
|
|
_port = null;
|
|
_responseBuffer.clear();
|
|
_isReading = false;
|
|
_cancelTimeout();
|
|
rethrow; // Re-throw the exception for UI handling
|
|
}
|
|
}
|
|
|
|
/// Disconnects from the currently connected USB device.
|
|
/// Cleans up resources: cancels timers, closes port, resets state.
|
|
// MODIFIED: Changed from void to Future<void> and added async.
|
|
Future<void> disconnect() async {
|
|
if (connectionState.value != SerialConnectionState.disconnected) {
|
|
debugPrint("SerialManager: Disconnecting...");
|
|
stopAutoReading(); // Stop any active auto-reading timer and timeout
|
|
// FIX: Update ValueNotifiers before closing the stream and port.
|
|
connectionState.value = SerialConnectionState.disconnected;
|
|
connectedDeviceName.value = null;
|
|
sondeId.value = null; // Clear Sonde ID on disconnect
|
|
await _port?.close(); // Now correctly awaiting the close operation
|
|
_port = null; // Clear port reference
|
|
_responseBuffer.clear(); // Clear any buffered data
|
|
_isReading = false; // Reset reading flag
|
|
_communicationLevel = 0; // Reset communication level
|
|
_parentAddress = null;
|
|
_serialNumber = null;
|
|
_parameterList.clear();
|
|
debugPrint("SerialManager: Disconnected.");
|
|
}
|
|
}
|
|
|
|
/// Starts a periodic timer to automatically request data from the device.
|
|
void startAutoReading({Duration interval = const Duration(seconds: 2)}) {
|
|
stopAutoReading(); // Stop any existing auto-reading timer first
|
|
if (connectionState.value == SerialConnectionState.connected) {
|
|
//startLiveReading(); // Initiate the first reading immediately
|
|
// Set up a periodic timer to call startLiveReading at the specified interval.
|
|
_dataRequestTimer = Timer.periodic(interval, (_) => startLiveReading());
|
|
debugPrint("SerialManager: Auto reading started with interval ${interval.inSeconds}s.");
|
|
} else {
|
|
debugPrint("SerialManager: Cannot start auto reading, not connected.");
|
|
}
|
|
}
|
|
|
|
/// Stops the automatic data refresh timer.
|
|
void stopAutoReading() {
|
|
_dataRequestTimer?.cancel(); // Cancel the periodic timer
|
|
_dataRequestTimer = null; // Clear timer reference
|
|
_cancelTimeout(); // Also cancel any pending response timeout
|
|
_isReading = false; // Ensure the reading flag is reset
|
|
debugPrint("SerialManager: Auto reading stopped.");
|
|
}
|
|
|
|
/// Initiates a full 3-step data reading sequence (Level 0, Level 1, Level 2).
|
|
/// This function prevents overlapping read cycles using the `_isReading` flag.
|
|
void startLiveReading() {
|
|
// Only proceed if connected and not already in a reading cycle
|
|
if (connectionState.value != SerialConnectionState.connected || _isReading) {
|
|
debugPrint("SerialManager: Cannot start live reading. Connected: ${connectionState.value == SerialConnectionState.connected}, Is Reading: $_isReading");
|
|
return;
|
|
}
|
|
_isReading = true; // Mark as busy to prevent re-entry
|
|
debugPrint("--- SerialManager: Starting New Read Cycle ---");
|
|
_communicationLevel = 0; // Start at communication level 0
|
|
_responseBuffer.clear(); // Clear the buffer for a fresh start
|
|
_lookupString = null; // Reset lookup string until a new command is sent
|
|
_sendCommand(0); // Send the Level 0 command
|
|
}
|
|
|
|
/// Callback function for the serial port's input stream.
|
|
/// Appends incoming data chunks to a buffer and attempts to process it.
|
|
void _onDataReceived(Uint8List data) {
|
|
if (data.isEmpty) return;
|
|
String responseHex = Converter.byteArrayToHexString(data);
|
|
_responseBuffer.write(responseHex); // Append to the shared buffer
|
|
// debugPrint("SerialManager: Received Chunk (Buffer: ${_responseBuffer.length} chars): $responseHex"); // Too verbose normally
|
|
_processBuffer(); // Attempt to process the buffer for complete messages
|
|
}
|
|
|
|
/// Attempts to extract and process complete messages from the `_responseBuffer`.
|
|
/// Handles partial messages, garbage data, and CRC validation.
|
|
void _processBuffer() {
|
|
String buffer = _responseBuffer.toString();
|
|
// Cannot process without an expected lookup string (set after sending a command)
|
|
if (_lookupString == null) {
|
|
// debugPrint("SerialManager: _processBuffer: No lookup string set. Waiting for command."); // Too verbose
|
|
return;
|
|
}
|
|
|
|
int startIndex = buffer.indexOf(_lookupString!); // Find the start of the expected message
|
|
if (startIndex == -1) {
|
|
// debugPrint("SerialManager: _processBuffer: Lookup string not found in buffer. Waiting for more data."); // Too verbose
|
|
return; // Expected start of message not found, wait for more data
|
|
}
|
|
|
|
// Discard any data before the expected start of the message (garbage or old data)
|
|
if (startIndex > 0) {
|
|
debugPrint("SerialManager: _processBuffer: Discarding ${startIndex} garbage characters from buffer.");
|
|
buffer = buffer.substring(startIndex); // Remove leading garbage
|
|
_responseBuffer.clear(); // Clear and rewrite the buffer to contain only valid data from startIndex
|
|
_responseBuffer.write(buffer);
|
|
}
|
|
|
|
// A minimum of 34 hex characters are needed to parse the dataBlockLength (offset 30, length 4)
|
|
if (buffer.length < 34) {
|
|
// debugPrint("SerialManager: _processBuffer: Buffer too short for header (${buffer.length} < 34). Waiting for more data."); // Too verbose
|
|
return; // Not enough data to even read the length field
|
|
}
|
|
|
|
try {
|
|
// Parse the data block length from the message header
|
|
int dataBlockLength = int.parse(buffer.substring(30, 34), radix: 16);
|
|
// Calculate the total expected length of the complete message (Header + Data Block + CRC)
|
|
int totalMessageLength = 34 + (dataBlockLength * 2) + 4; // 34 chars for header, dataBlockLength bytes * 2 chars/byte, 4 chars for CRC
|
|
|
|
// If the buffer contains a complete message
|
|
if (buffer.length >= totalMessageLength) {
|
|
_cancelTimeout(); // Message received, cancel the response timeout timer
|
|
String completeResponse = buffer.substring(0, totalMessageLength);
|
|
|
|
// --- CRC Validation ---
|
|
Uint8List responseBytes = Converter.hexStringToByteArray(completeResponse);
|
|
// CRC is calculated over the entire message *excluding* the final 2 CRC bytes themselves
|
|
Uint8List dataBytesForCrc = responseBytes.sublist(0, responseBytes.length - 2);
|
|
String calculatedCrc = Crc16Ccitt.computeCrc16Ccitt(dataBytesForCrc);
|
|
String receivedCrc = completeResponse.substring(completeResponse.length - 4); // Last 4 hex chars are the CRC
|
|
|
|
if (calculatedCrc.toUpperCase() != receivedCrc.toUpperCase()) {
|
|
debugPrint("SerialManager: CRC Mismatch! Calculated: $calculatedCrc, Received: $receivedCrc for response: $completeResponse. Discarding message.");
|
|
// Discard the invalid message from the buffer and attempt to process the rest
|
|
_responseBuffer.clear();
|
|
_responseBuffer.write(buffer.substring(totalMessageLength));
|
|
_isReading = false; // Consider ending the cycle or signaling an error that might require a retry.
|
|
if(_responseBuffer.isNotEmpty) _processBuffer(); // Check if more messages are in the buffer
|
|
return;
|
|
}
|
|
|
|
// Message is valid. Remove it from the buffer.
|
|
_responseBuffer.clear();
|
|
_responseBuffer.write(buffer.substring(totalMessageLength));
|
|
|
|
debugPrint("SerialManager: Processing Full Valid Response (Lvl: $_communicationLevel): $completeResponse");
|
|
_handleResponse(completeResponse); // Route the complete message for parsing
|
|
|
|
// Recursively call _processBuffer in case multiple messages arrived in one chunk,
|
|
// or a new message started immediately after the processed one.
|
|
if(_responseBuffer.isNotEmpty) _processBuffer();
|
|
}
|
|
} catch (e) {
|
|
debugPrint("SerialManager: Buffer processing error: $e. Resetting cycle and clearing buffer.");
|
|
_responseBuffer.clear();
|
|
_isReading = false; // End this cycle on error
|
|
}
|
|
}
|
|
|
|
/// Dispatches the complete and validated response string to the appropriate handler
|
|
/// based on the current communication level.
|
|
void _handleResponse(String response) {
|
|
// --- REVISED: Added null check before dispatching ---
|
|
if (_isDisposed) {
|
|
debugPrint("SerialManager: Ignoring response on disposed manager.");
|
|
return;
|
|
}
|
|
|
|
switch (_communicationLevel) {
|
|
case 0:
|
|
_handleResponseLevel0(response);
|
|
break;
|
|
case 1:
|
|
_handleResponseLevel1(response);
|
|
break;
|
|
case 2:
|
|
_handleResponseLevel2(response);
|
|
break;
|
|
default:
|
|
debugPrint("SerialManager: Unknown communication level: $_communicationLevel for response: $response");
|
|
_isReading = false; // Ensure reading lock is released if level is invalid
|
|
}
|
|
}
|
|
|
|
/// Handles and parses the response for communication Level 0.
|
|
/// Extracts parent address and serial number.
|
|
void _handleResponseLevel0(String responseHex) {
|
|
try {
|
|
// Minimum length check to ensure substrings are safe
|
|
// 34 (header) + 40 (data block for L0) + 4 (CRC) = 78 expected min length for full response
|
|
// It looks like your previous code assumed data block starts at 34 and contains 4 bytes (8 chars)
|
|
// for parent address at offset 86, and 9 bytes (18 chars) for serial at 68.
|
|
// Let's refine based on typical structure.
|
|
// If `responseHex.substring(68, 86)` and `responseHex.substring(86, 94)` are correct from your sensor,
|
|
// then total length for serial number (18) + parent address (8) + other header/footer must be respected.
|
|
// 18 chars for serial, 8 chars for parent. Total 26 chars / 2 = 13 bytes data length.
|
|
// So, total expected response length = 34 + (13*2) + 4 = 34 + 26 + 4 = 64.
|
|
// If your sensor gives 94 chars: 34 (header) + (94-34-4=56 data chars) + 4 (CRC) -> 28 bytes data.
|
|
// This means offsets 68-86 and 86-94 might be within a larger data block.
|
|
// This response example is for a specific sensor protocol. Adjust offsets if yours differs.
|
|
if (responseHex.length < 94) { // Assuming 94 is the correct full length including all L0 data.
|
|
debugPrint("SerialManager: Error: Level 0 response too short. Length: ${responseHex.length} (expected at least 94)");
|
|
_isReading = false;
|
|
return;
|
|
}
|
|
|
|
// Re-evaluate based on your specific protocol:
|
|
// The offsets 68-86 and 86-94 suggest a specific structure within the *data block* of the response.
|
|
// The `dataBlockLength` field (at 30-34) specifies the length of the data portion *after* the header.
|
|
// Let's assume these substrings are correct as per your sensor's documentation:
|
|
_parentAddress = responseHex.substring(86, 94); // Extract parent address (8 hex chars = 4 bytes)
|
|
_serialNumber = Converter.hexToAscii(responseHex.substring(68, 86)); // Extract serial number (18 hex chars = 9 bytes, then convert to ASCII)
|
|
|
|
debugPrint("SerialManager: Parsed L0 -> Address: $_parentAddress, Serial: $_serialNumber");
|
|
|
|
// --- Update sondeId ValueNotifier here ---
|
|
// --- REVISED: Added null check before updating ValueNotifier ---
|
|
if (!_isDisposed) {
|
|
if (_serialNumber != null && _serialNumber!.isNotEmpty) {
|
|
sondeId.value = _serialNumber;
|
|
debugPrint("SerialManager: Updated Sonde ID: ${sondeId.value}");
|
|
} else {
|
|
sondeId.value = "N/A"; // Or handle empty/null serial number appropriately
|
|
}
|
|
}
|
|
|
|
|
|
if (_parentAddress != "00000000") { // Check for valid parent address
|
|
_communicationLevel = 1; // Advance to Level 1
|
|
// Delay before sending next command to allow device time to process
|
|
Future.delayed(const Duration(milliseconds: 200), () => _sendCommand(1));
|
|
} else {
|
|
debugPrint("SerialManager: Parent address is 00000000, not proceeding to Level 1. Ending cycle.");
|
|
_isReading = false; // End the cycle if parent address is invalid
|
|
}
|
|
} catch (e) {
|
|
debugPrint("SerialManager: Error parsing L0: $e");
|
|
_isReading = false; // End this cycle on error
|
|
}
|
|
}
|
|
|
|
/// Handles and parses the response for communication Level 1.
|
|
/// Extracts the list of available parameters.
|
|
void _handleResponseLevel1(String responseHex) {
|
|
try {
|
|
if (responseHex.length < 34) {
|
|
debugPrint("SerialManager: Error: Level 1 response too short for dataBlockLength. Length: ${responseHex.length}");
|
|
_isReading = false;
|
|
return;
|
|
}
|
|
int dataBlockLength = int.parse(responseHex.substring(30, 34), radix: 16);
|
|
int expectedParamsBlockEnd = 34 + (dataBlockLength * 2);
|
|
|
|
if (expectedParamsBlockEnd > responseHex.length) {
|
|
debugPrint("SerialManager: Error: Level 1 parameters block calculated end index ($expectedParamsBlockEnd) exceeds received data length (${responseHex.length}). dataBlockLength was: $dataBlockLength (0x${dataBlockLength.toRadixString(16).padLeft(4, '0')})");
|
|
_isReading = false;
|
|
return;
|
|
}
|
|
|
|
String paramsBlock = responseHex.substring(34, expectedParamsBlockEnd);
|
|
_parameterList.clear();
|
|
|
|
// Each parameter entry is 6 hex characters (3 bytes).
|
|
// Parameter code (2 bytes) is typically at index 2-5 within each 6-char block.
|
|
// Example: '005A00' -> code '5A00'
|
|
for (int i = 0; i <= paramsBlock.length - 6; i += 6) {
|
|
String parameterCode = paramsBlock.substring(i + 2, i + 6);
|
|
_parameterList.add(ParameterHelper.getDescription(parameterCode));
|
|
}
|
|
debugPrint("SerialManager: Parsed L1 -> Parameters: $_parameterList");
|
|
_communicationLevel = 2; // Advance to Level 2
|
|
Future.delayed(const Duration(milliseconds: 200), () => _sendCommand(2));
|
|
} catch (e) {
|
|
debugPrint("SerialManager: Error parsing L1: $e");
|
|
_isReading = false;
|
|
}
|
|
}
|
|
|
|
/// Handles and parses the response for communication Level 2.
|
|
/// Extracts the actual values for the previously identified parameters.
|
|
void _handleResponseLevel2(String responseHex) {
|
|
try {
|
|
if (responseHex.length < 34) {
|
|
debugPrint("SerialManager: Error: Level 2 response too short for dataBlockLength. Length: ${responseHex.length}");
|
|
_isReading = false;
|
|
return;
|
|
}
|
|
int dataBlockLength = int.parse(responseHex.substring(30, 34), radix: 16);
|
|
int expectedValuesBlockEnd = 34 + (dataBlockLength * 2);
|
|
|
|
if (expectedValuesBlockEnd > responseHex.length) {
|
|
debugPrint("SerialManager: Error: Level 2 values block calculated end index ($expectedValuesBlockEnd) exceeds received data length (${responseHex.length}). dataBlockLength was: $dataBlockLength (0x${dataBlockLength.toRadixString(16).padLeft(4, '0')})");
|
|
_isReading = false;
|
|
return;
|
|
}
|
|
|
|
String valuesBlock = responseHex.substring(34, expectedValuesBlockEnd);
|
|
List<double> values = [];
|
|
|
|
// Each value is 8 hex characters (4 bytes) representing a float
|
|
for (int i = 0; i <= valuesBlock.length - 8; i += 8) {
|
|
String valueHex = valuesBlock.substring(i, i + 8);
|
|
try {
|
|
values.add(Converter.hexToFloat(valueHex));
|
|
} catch (e) {
|
|
debugPrint("SerialManager: Error converting hex '$valueHex' to float in Level 2: $e");
|
|
values.add(double.nan); // Add NaN or a default error value if conversion fails
|
|
}
|
|
}
|
|
|
|
// Ensure the number of parsed parameters matches the number of parsed values.
|
|
if (_parameterList.length == values.length) {
|
|
Map<String, double> finalReadings = Map.fromIterables(_parameterList, values);
|
|
// --- REVISED: Added null check before adding to the stream controller ---
|
|
if (!_isDisposed) {
|
|
_dataStreamController.add(finalReadings); // Broadcast the final parsed readings
|
|
}
|
|
debugPrint("SerialManager: Final Parsed Readings: $finalReadings");
|
|
} else {
|
|
debugPrint("SerialManager: L2 Data Mismatch: ${values.length} values for ${_parameterList.length} parameters. Parameter list: $_parameterList, Values: $values");
|
|
}
|
|
} catch (e) {
|
|
debugPrint("SerialManager: Error parsing L2: $e");
|
|
} finally {
|
|
_isReading = false; // Mark the read cycle as complete, allowing the next one to start
|
|
debugPrint("--- SerialManager: Read Cycle Complete ---");
|
|
}
|
|
}
|
|
|
|
/// Constructs and sends the appropriate command based on the current communication level.
|
|
void _sendCommand(int level) async {
|
|
// Generate a sequence number (0-255), padded to 2 hex characters.
|
|
String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0').toUpperCase();
|
|
|
|
// Set the lookup string for the expected response. This is the start of the message
|
|
// that the `_processBuffer` will look for to identify a complete response.
|
|
_lookupString = '7E02${seqNo}'; // Assuming responses start with 7E02 and the same sequence number.
|
|
|
|
String commandBody;
|
|
switch (level) {
|
|
case 0:
|
|
commandBody = '0000000002000000200000010000'; // Command for Level 0
|
|
break;
|
|
case 1:
|
|
if (_parentAddress == null) {
|
|
debugPrint("SerialManager: Error: _parentAddress is null for Level 1 command. Cannot send. Ending cycle.");
|
|
_isReading = false; // Release the reading lock
|
|
// Call disconnect (now async void) without await here as we don't need to block
|
|
disconnect(); // Disconnect due to invalid state
|
|
return;
|
|
}
|
|
commandBody = '${_parentAddress}02000000200000180000'; // Command for Level 1, includes parent address
|
|
break;
|
|
case 2:
|
|
if (_parentAddress == null) {
|
|
debugPrint("SerialManager: Error: _parentAddress is null for Level 2 command. Cannot send. Ending cycle.");
|
|
_isReading = false; // Release the reading lock
|
|
// Call disconnect (now async void) without await here as we don't need to block
|
|
disconnect(); // Disconnect due to invalid state
|
|
return;
|
|
}
|
|
commandBody = '${_parentAddress}02000000200000190000'; // Command for Level 2, includes parent address
|
|
break;
|
|
default:
|
|
debugPrint("SerialManager: Attempted to send command for unknown level: $level. Resetting cycle.");
|
|
_isReading = false;
|
|
return;
|
|
}
|
|
|
|
String commandHex = '7E02$seqNo$commandBody'; // Full command without CRC
|
|
Uint8List commandBytes = Converter.hexStringToByteArray(commandHex); // Convert command hex to bytes
|
|
String crc = Crc16Ccitt.computeCrc16Ccitt(commandBytes); // Compute CRC for the command bytes
|
|
String finalPacket = commandHex + crc; // Append CRC to the command
|
|
|
|
try {
|
|
await _port?.write(Converter.hexStringToByteArray(finalPacket)); // Send the full packet over serial
|
|
debugPrint("SerialManager: Sent (Lvl: $level): $finalPacket (Expecting lookup: $_lookupString)");
|
|
|
|
// Set a timeout. If no valid response is received within this duration,
|
|
// it's assumed the command failed, and the reading cycle is reset.
|
|
_cancelTimeout(); // Cancel any previous timeout
|
|
_responseTimeoutTimer = Timer(const Duration(seconds: 3), () {
|
|
debugPrint("SerialManager: Response timeout for Level $level. Unlocking for next cycle.");
|
|
_isReading = false; // Allow the next read cycle to start
|
|
_responseBuffer.clear(); // Clear buffer on timeout to avoid stale or partial data confusing future reads
|
|
// Optionally, you might want to trigger a retry for the current level here
|
|
// or just let the auto-reading timer trigger the next full cycle.
|
|
});
|
|
} catch (e) {
|
|
debugPrint("SerialManager: Error sending command (Lvl: $level): $e. Disconnecting.");
|
|
_isReading = false; // Release the reading lock on send error
|
|
// Call disconnect (now async void) without await here as we don't need to block
|
|
disconnect(); // Disconnect if sending fails
|
|
}
|
|
}
|
|
|
|
/// Cancels the current response timeout timer if it's active.
|
|
void _cancelTimeout() {
|
|
_responseTimeoutTimer?.cancel();
|
|
_responseTimeoutTimer = null;
|
|
}
|
|
|
|
/// Cleans up all resources held by the SerialManager when it's no longer needed.
|
|
void dispose() {
|
|
debugPrint("SerialManager: Disposing.");
|
|
_isDisposed = true; // Set the flag immediately
|
|
disconnect(); // Ensure full disconnection and cleanup
|
|
_dataStreamController.close(); // Close the data stream controller
|
|
// FIX: Dispose of all ValueNotifiers to prevent "used after dispose" errors
|
|
connectionState.dispose();
|
|
connectedDeviceName.dispose();
|
|
sondeId.dispose();
|
|
}
|
|
} |