Created
April 8, 2024 11:39
-
-
Save trabulium/ca46680c031b1d8cee7439aa62413cfd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:typed_data'; | |
import 'dart:async'; | |
import 'dart:math' as math; | |
enum AcknowledgementResult { | |
AckReceived, | |
NakReceived, | |
CrcCommand, | |
EotReceived, | |
CancelReceived, | |
AbortReceived, | |
Timeout, | |
OtherError, | |
} | |
class YModem { | |
Completer<void>? _crcCommandReceivedCompleter; | |
static final YModem _instance = YModem._internal(); | |
final StreamController<Uint8List> _incomingDataController = | |
StreamController<Uint8List>.broadcast(); | |
StreamSubscription<Uint8List>? _dataSubscription; | |
factory YModem() => _instance; | |
YModem._internal() { | |
// Consider moving subscription logic out of the constructor | |
// and into a method that can be explicitly called when needed. | |
} | |
void startListening() { | |
_dataSubscription = _incomingDataController.stream.listen( | |
(data) { | |
_debugPrint('Direct Stream Data: $data'); | |
switch (data[0]) { | |
case 0x06: // ACK | |
_ackReceived = true; | |
break; | |
case 0x15: // NAK | |
_nakReceived = true; | |
break; | |
case 0x43: // 'C' for CRC-16 | |
_crcCommandReceived = true; | |
break; | |
case 0x04: // EOT | |
_eotReceived = true; | |
break; | |
case 0x18: // CA (cancel) | |
_cancelReceived = true; | |
break; | |
case 0x41: // CA (Abort) | |
_abortReceived = true; | |
break; | |
default: | |
_debugPrint('Unknown command received'); | |
break; | |
} | |
}, | |
onError: (error) { | |
_debugPrint('Direct Stream Error: $error'); | |
}, | |
onDone: () { | |
_debugPrint('Stream closed'); | |
}, | |
); | |
} | |
void stopListening() { | |
_dataSubscription?.cancel(); | |
} | |
// Flag to enable/disable debug logging | |
bool _debugEnabled = true; | |
// A flag to indicate acknowledgment status | |
bool _ackReceived = false; | |
bool _nakReceived = false; | |
bool _crcCommandReceived = false; | |
bool _eotReceived = false; | |
bool _cancelReceived = false; | |
bool _abortReceived = false; | |
void dispose() { | |
_incomingDataController.close(); | |
} | |
// Method to toggle debug logging | |
void setDebug(bool enabled) { | |
_debugEnabled = enabled; | |
} | |
// Debug logging function | |
void _debugPrint(String message) { | |
if (_debugEnabled) { | |
print(message); | |
} | |
} | |
// Reset all flags to their initial state | |
void _resetFlags() { | |
_ackReceived = false; | |
_nakReceived = false; | |
_crcCommandReceived = false; | |
_eotReceived = false; | |
_cancelReceived = false; | |
_abortReceived = false; | |
} | |
Future<void> onSendData(Uint8List data, bool withoutResponse) async { | |
// Assuming `bluetoothManager` is accessible within this class and has been properly initialized | |
print('Sending data: $data'); | |
bool isYModemTransfer = true; | |
await bluetoothManager.sendYModemData(data, isYModemTransfer); | |
} | |
Future<void> initiateTransfer() async { | |
// Send the initial command to signal the start of YModem transfer. | |
// For example, sending '|' to indicate the start. | |
_resetFlags(); | |
final Uint8List initiator = Uint8List.fromList([0x7C]); | |
await onSendData(initiator, true); // ASCII for '|' is 124 | |
_debugPrint("Initiating transfer"); | |
// Optionally, here you might wait for a 'C' from the receiver, | |
// indicating readiness for CRC-16 blocks. | |
// This waiting can be handled outside this function or within | |
// if you have a mechanism to receive and process incoming data. | |
} | |
void initiateTransferAndListen() { | |
_crcCommandReceivedCompleter = | |
Completer<void>(); // Initialize the completer | |
_incomingDataController.stream.listen((data) { | |
switch (data[0]) { | |
case 0x06: // ACK | |
_ackReceived = true; | |
break; | |
case 0x15: // NAK | |
_nakReceived = true; | |
break; | |
case 0x43: // 'C' for CRC-16 | |
_crcCommandReceived = true; | |
if (!_crcCommandReceivedCompleter!.isCompleted) { | |
_crcCommandReceivedCompleter!.complete(); // Complete the completer | |
} | |
break; | |
case 0x04: // EOT | |
_eotReceived = true; | |
break; | |
case 0x18: // CA (cancel) | |
_cancelReceived = true; | |
break; | |
case 0x41: // CA (abort) | |
_abortReceived = true; | |
break; | |
default: | |
_debugPrint('Unknown command received'); | |
break; | |
} | |
}, onError: (error) { | |
_debugPrint('Stream error: $error'); | |
}, onDone: () { | |
_debugPrint('Stream closed'); | |
}); | |
initiateTransfer(); | |
} | |
/// verify code Crc16Xmodem | |
int _crc16CcittXmodem(Uint8List bytes) { | |
//_debugPrint('Data for CRC: ${bytes.map((e) => e.toString()).join(', ')}'); | |
// Calculate the CRC-16/XMODEM checksum for the given data | |
//Width: 16 bits | |
//Polynomial: 0x1021 (standard for CRC-16-CCITT) | |
//Initial Value: 0xFFFF (common starting value for CRC-16-CCITT) | |
//Final XOR Value: 0x0000 (commonly used in CRC-16-CCITT to not alter the final CRC result) | |
//Input Reflected: false (data processed as-is, which is typical for CRC-16-CCITT) | |
//Output Reflected: false (CRC output as-is, which is typical for CRC-16-CCITT) | |
final crcResult = calculateCrc16Ccitt(bytes); | |
// Assuming the result can be directly converted to an integer representation. | |
int crcValue = int.tryParse(crcResult.toRadixString(16), radix: 16) ?? 0; | |
print('Calculated CRC: 0x${crcValue.toRadixString(16).toUpperCase()}'); | |
return crcValue; | |
} | |
int calculateCrc16Ccitt(List<int> data) { | |
int crc = 0xFFFF; | |
for (int byte in data) { | |
crc ^= byte << 8; | |
for (int i = 0; i < 8; i++) { | |
if ((crc & 0x8000) != 0) { | |
crc = (crc << 1) ^ 0x1021; | |
} else { | |
crc <<= 1; | |
} | |
crc &= 0xFFFF; // Ensure CRC remains a 16-bit value | |
} | |
} | |
return crc; | |
} | |
List<int> crcToBytes(int crc) { | |
int highByte = crc >> 8; | |
int lowByte = crc & 0xFF; | |
return [highByte, lowByte]; | |
} | |
Future<List<Uint8List>> prepareFileData( | |
Uint8List fileData, String fileName) async { | |
const int blockSize = 128; | |
List<Uint8List> blocks = []; | |
// Escape control characters if required in the entire file data first | |
bool needToEscape = false; | |
Uint8List escapedFileData = escapeControlCharacters(fileData, needToEscape); | |
// Use "firmware.bin" as the filename, ignoring the fileName argument | |
String fixedFileName = "firmware.bin"; | |
String fileSizeString = fileData.length.toString(); | |
Uint8List headerBlockData = Uint8List.fromList( | |
fixedFileName.codeUnits + [0] + fileSizeString.codeUnits + [0]); | |
// Ensure the header block fits into the blockSize, filled with 0x1A | |
int paddingLength = blockSize - headerBlockData.length + 2; | |
Uint8List paddedHeaderBlockData = | |
Uint8List.fromList(headerBlockData + List.filled(paddingLength, 0x1A)); | |
// Add the header block to blocks list | |
blocks.add(prepareBlock( | |
paddedHeaderBlockData, 0)); // Header block with sequence number 0 | |
// Divide the escaped file data into blocks | |
int totalBlocks = (escapedFileData.length / blockSize).ceil(); | |
for (int i = 0; i < totalBlocks; i++) { | |
int start = i * blockSize; | |
int end = math.min(start + blockSize, escapedFileData.length); | |
Uint8List blockData = escapedFileData.sublist(start, end); | |
// Prepare each data block with its sequence number | |
blocks.add(prepareBlock( | |
blockData, i + 1)); // Data blocks start with sequence number 1 | |
} | |
return blocks; | |
} | |
Uint8List prepareBlock(Uint8List blockData, int sequenceNumber) { | |
const int blockSize = 128; | |
Uint8List block = | |
Uint8List(blockSize + 5); // SOH, sequence number, complement, data, CRC | |
block[0] = 0x01; // SOH | |
block[1] = sequenceNumber % 256; // Sequence number | |
block[2] = 255 - block[1]; // Sequence number complement | |
if (blockData.length < blockSize) { | |
blockData = Uint8List.fromList(blockData + | |
List.filled(blockSize - blockData.length, 0x1A)); // Padding | |
} | |
block.setRange(3, blockSize + 3, blockData); // Set data | |
// Calculate and set CRC | |
int crc = _crc16CcittXmodem(blockData); | |
//only send CRC after first block | |
if (sequenceNumber > 0) { | |
block[blockSize + 3] = crc >> 8; // CRC high byte | |
block[blockSize + 4] = crc & 0xFF; // CRC low byte | |
} | |
return block; | |
} | |
Uint8List escapeControlCharacters(Uint8List data, bool needToEscape) { | |
if (!needToEscape) { | |
// If no need to escape, return the original data | |
return data; | |
} | |
List<int> escapedData = []; | |
for (int byte in data) { | |
switch (byte) { | |
case 0x0D: // CR | |
case 0x0A: // LF | |
escapedData.add(0x1B); // ESC | |
escapedData.add(byte == 0x0D ? 0x2D : 0x2A); // Escaped CR or LF | |
break; | |
default: | |
escapedData.add(byte); | |
break; | |
} | |
} | |
return Uint8List.fromList(escapedData); | |
} | |
Future<void> sendBlock( | |
Uint8List block, { | |
required Function(List<int> data, bool YModemSending) onSendData, | |
}) async { | |
// Send a single data block to the receiver. | |
await onSendData(block.toList(), true); | |
} | |
Future<AcknowledgementResult> waitForAcknowledgement( | |
{int timeoutSeconds = 3}) async { | |
final startTime = DateTime.now(); | |
while (!_ackReceived && | |
!_nakReceived && | |
!_crcCommandReceived && | |
!_eotReceived && | |
!_cancelReceived && | |
!_abortReceived) { | |
await Future.delayed(Duration(milliseconds: 5)); | |
if (DateTime.now().difference(startTime).inSeconds > timeoutSeconds) { | |
_debugPrint("Timeout waiting for acknowledgment"); | |
return AcknowledgementResult.Timeout; | |
} | |
} | |
if (_ackReceived) { | |
_debugPrint("ACK received, proceeding with next block"); | |
return AcknowledgementResult.AckReceived; | |
} else if (_nakReceived) { | |
_debugPrint("NAK received, need to resend the last block"); | |
return AcknowledgementResult.NakReceived; | |
} else if (_crcCommandReceived) { | |
_debugPrint( | |
"'C' received, ready for next block or start of transmission"); | |
return AcknowledgementResult.CrcCommand; // Adjusted return value | |
} else if (_eotReceived) { | |
_debugPrint("EOT received, ending transmission"); | |
return AcknowledgementResult.EotReceived; | |
} else if (_cancelReceived) { | |
_debugPrint("Transfer canceled by receiver"); | |
return AcknowledgementResult.CancelReceived; | |
} else if (_abortReceived) { | |
_debugPrint("Transfer aborted by receiver"); | |
return AcknowledgementResult.AbortReceived; | |
} else { | |
// Handle unexpected cases (shouldn't reach here) | |
_debugPrint("Unexpected error during acknowledgment"); | |
return AcknowledgementResult.OtherError; | |
} | |
} | |
Future<bool> shouldRetry(int currentAttempt, int maxRetries, | |
AcknowledgementResult acknowledgementResult) async { | |
// Check for retryable acknowledgment results | |
_debugPrint( | |
'Current attempt: $currentAttempt vs Max attempts: $maxRetries'); | |
if (acknowledgementResult == AcknowledgementResult.Timeout || | |
acknowledgementResult == AcknowledgementResult.NakReceived) { | |
// Retry on timeouts and NAKs (adjust retry logic as needed) | |
if (currentAttempt < maxRetries) { | |
_debugPrint("Retrying block due to $acknowledgementResult"); | |
return true; | |
} else { | |
_abortReceived = true; | |
_debugPrint("Maximum retries reached for block"); | |
return false; | |
} | |
} else { | |
// Handle other acknowledgment results (e.g., log them, notify the user, or decide not to retry) | |
_debugPrint( | |
"Non-retryable acknowledgment result: $acknowledgementResult"); | |
return false; | |
} | |
} | |
Future<void> finaliseTransfer({ | |
required Future<void> Function(Uint8List data, bool withoutResponse) | |
onSendData, | |
}) async { | |
// Define the EOT byte. | |
final Uint8List eot = Uint8List.fromList([0x04]); | |
//await onSendData(eot, true); | |
bool isYModemTransfer = false; //because we're finalizing the transfer | |
await bluetoothManager.sendYModemData(eot, isYModemTransfer); | |
// Wait for an acknowledgment, handling different outcomes | |
final acknowledgementResult = await waitForAcknowledgement(); | |
switch (acknowledgementResult) { | |
case AcknowledgementResult.AckReceived: | |
_debugPrint('Final ACK received after EOT.'); | |
break; | |
case AcknowledgementResult.Timeout: | |
_debugPrint('Timeout waiting for final ACK after EOT.'); | |
// Consider retrying or handling the timeout appropriately | |
break; | |
case AcknowledgementResult.NakReceived: | |
_debugPrint('NAK received after EOT.'); | |
// Consider retrying or handling the NAK appropriately | |
break; | |
case AcknowledgementResult.CrcCommand: | |
// Handle CRC command received (this might not be expected here) | |
_debugPrint('Unexpected CRC command received after EOT.'); | |
break; | |
case AcknowledgementResult.EotReceived: | |
// Handle EOT received (this might indicate a protocol issue) | |
_debugPrint('Unexpected EOT received after sending EOT.'); | |
break; | |
case AcknowledgementResult.CancelReceived: | |
// Handle transfer cancellation | |
_debugPrint('Transfer canceled by receiver after EOT.'); | |
break; | |
case AcknowledgementResult.AbortReceived: | |
_debugPrint("Transfer aborted by receiver"); | |
break; | |
case AcknowledgementResult.OtherError: | |
// Handle other unexpected errors | |
_debugPrint('Unexpected error during final acknowledgment.'); | |
break; | |
} | |
// Perform any additional finalization steps required by your application or the protocol. | |
_debugPrint('YModem transfer finalized.'); | |
} | |
void processIncomingData(Uint8List data) { | |
// Simply add the incoming data to the stream | |
//_debugPrint('Incoming Received: $data'); | |
_incomingDataController.add(data); | |
} | |
void runYModemTransfer({ | |
required Uint8List fileData, | |
required String fileName, | |
required void Function(double progress) onProgress, | |
required Future<void> Function(Uint8List data, bool withoutResponse) | |
onSendData, | |
}) async { | |
var startTime = DateTime.now(); | |
int maxRetries = 5; // Set a maximum number of attempts | |
int currentAttempt = 1; // Track the current attempt number | |
int notificationProgress = 0; | |
bool sendFailed = false; | |
// Assume blocks are prepared from fileData | |
var blocks = await prepareFileData(fileData, fileName); | |
initiateTransferAndListen(); | |
// Wait for the 'C' command to be received | |
await _crcCommandReceivedCompleter | |
?.future; // This will pause execution here until 'C' is received | |
_debugPrint('CRC Command Received: ' + _crcCommandReceived.toString()); | |
_incomingDataController.stream.listen((data) { | |
print('Ymodem Response: ' + | |
data.toString() + | |
" Progress: " + | |
notificationProgress.toString()); | |
notificationProgress++; | |
switch (data[0]) { | |
case 0x06: // ACK | |
_ackReceived = true; | |
break; | |
case 0x15: // NAK | |
_nakReceived = true; | |
_debugPrint('NAK == $_nakReceived command received'); | |
break; | |
case 0x43: // 'C' for CRC-16 | |
_crcCommandReceived = true; | |
break; | |
case 0x04: // EOT | |
_eotReceived = true; | |
break; | |
case 0x18: // CA (cancel) | |
_cancelReceived = true; | |
break; | |
default: | |
_debugPrint('Unknown command received'); | |
break; | |
} | |
}, onError: (error) { | |
_debugPrint('Stream error: $error'); | |
}, onDone: () { | |
_debugPrint('Stream closed'); | |
}); | |
await _crcCommandReceivedCompleter?.future; | |
_debugPrint('CRC Command Received: ' + _crcCommandReceived.toString()); | |
if (_crcCommandReceived == true) { | |
for (var index = 0; index < blocks.length; index++) { | |
var blockStartTime = DateTime.now(); | |
try { | |
_resetFlags(); | |
_debugPrint( | |
'Beginning block $index transmission' + blocks[index].toString()); | |
var currentBlock = blocks[index]; | |
await onSendData(currentBlock, true); | |
var blockEndTime = DateTime.now(); | |
_debugPrint( | |
'Block $index sent in ${blockEndTime.difference(blockStartTime).inMilliseconds} ms'); | |
// Wait for an acknowledgment | |
final acknowledgementResult = AcknowledgementResult.AckReceived; | |
await waitForAcknowledgement(); | |
if (acknowledgementResult == AcknowledgementResult.Timeout || | |
acknowledgementResult == AcknowledgementResult.NakReceived) { | |
// Handle non-ACK responses | |
_debugPrint( | |
'Acknowledgement for block $index was: $acknowledgementResult'); | |
if (await shouldRetry( | |
currentAttempt, maxRetries, acknowledgementResult)) { | |
currentAttempt++; | |
index--; | |
//await Future.delayed(Duration(milliseconds: 100)); | |
continue; // Retry the current block | |
} else { | |
_debugPrint('Retry failed for block $index'); | |
bluetoothManager.sendYModemData(Spinfire.END_YMODEM, false); | |
sendFailed = true; | |
stopListening(); | |
break; | |
// Consider handling the failure appropriately (e.g., break the loop) | |
} | |
} else if (acknowledgementResult == | |
AcknowledgementResult.CrcCommand) { | |
// Handle CRC command received during block transmission | |
} else if (acknowledgementResult != | |
AcknowledgementResult.AckReceived) { | |
// Handle other acknowledgment results (CrcCommand, EotReceived, CancelReceived, OtherError) | |
_debugPrint( | |
'Unexpected acknowledgement result ($acknowledgementResult) for block $index'); | |
bluetoothManager.sendYModemData(Spinfire.END_YMODEM, false); | |
sendFailed = true; | |
stopListening(); | |
break; | |
} | |
// Reset attempt count after successful transmission | |
currentAttempt = 1; | |
// Update progress after successful block transmission | |
onProgress((index + 1) / blocks.length); | |
//await Future.delayed(Duration(milliseconds: 500)); | |
} catch (e) { | |
_debugPrint('Error sending block $index: $e'); | |
} | |
} | |
var endTime = DateTime.now(); | |
_debugPrint( | |
'Total YModem transfer time: ${endTime.difference(startTime).inSeconds} seconds'); | |
// After all blocks are sent, handle transfer completion (e.g., send EOT) | |
if (!sendFailed) { | |
await finaliseTransfer(onSendData: onSendData); | |
notificationProgress = 0; | |
stopListening(); | |
} | |
} | |
} | |
} | |
class CRCException implements Exception { | |
final String message; | |
CRCException(this.message); | |
@override | |
String toString() => 'CRCException: $message'; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment