Skip to content

Instantly share code, notes, and snippets.

@trabulium
Created April 8, 2024 11:39
Show Gist options
  • Save trabulium/ca46680c031b1d8cee7439aa62413cfd to your computer and use it in GitHub Desktop.
Save trabulium/ca46680c031b1d8cee7439aa62413cfd to your computer and use it in GitHub Desktop.
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