import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:abawo_bt_app/model/firmware_file_selection.dart'; import 'package:abawo_bt_app/model/shifter_types.dart'; import 'package:abawo_bt_app/service/dfu_protocol.dart'; import 'package:file_picker/file_picker.dart'; typedef SessionIdGenerator = int Function(); class FirmwarePickerSelection { const FirmwarePickerSelection({ required this.fileName, required this.fileBytes, this.filePath, }); final String fileName; final Uint8List fileBytes; final String? filePath; } abstract interface class FirmwareFilePicker { Future pickFirmwareFile(); } class LocalFirmwareFilePicker implements FirmwareFilePicker { @override Future pickFirmwareFile() async { final pickResult = await FilePicker.platform.pickFiles( allowMultiple: false, withData: true, type: FileType.custom, allowedExtensions: const ['bin'], ); if (pickResult == null) { return null; } if (pickResult.files.isEmpty) { return FirmwarePickerSelection( fileName: '', fileBytes: Uint8List(0), ); } final selected = pickResult.files.first; final bytes = selected.bytes ?? await _readFromPath(selected.path); return FirmwarePickerSelection( fileName: selected.name, filePath: selected.path, fileBytes: bytes, ); } Future _readFromPath(String? path) async { if (path == null || path.trim().isEmpty) { throw const FileSystemException( 'Selected file did not contain readable bytes or a valid path.', ); } final file = File(path); return file.readAsBytes(); } } class FirmwareFileSelectionService { FirmwareFileSelectionService({ required FirmwareFilePicker filePicker, SessionIdGenerator? sessionIdGenerator, }) : _filePicker = filePicker, _sessionIdGenerator = sessionIdGenerator ?? _randomSessionId; final FirmwareFilePicker _filePicker; final SessionIdGenerator _sessionIdGenerator; Future selectAndPrepareBootloaderDfu() async { final FirmwarePickerSelection? selection; try { selection = await _filePicker.pickFirmwareFile(); } catch (error) { return FirmwareFileSelectionResult.failed( FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.readFailed, message: 'Could not read selected firmware file: $error', ), ); } if (selection == null) { return FirmwareFileSelectionResult.failed( const FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.canceled, message: 'Firmware selection canceled.', ), ); } final fileName = selection.fileName.trim(); if (fileName.isEmpty) { return FirmwareFileSelectionResult.failed( const FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.malformedSelection, message: 'Selected firmware file is missing a valid name.', ), ); } if (!_hasBinExtension(fileName)) { return FirmwareFileSelectionResult.failed( FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.unsupportedExtension, message: 'Unsupported firmware file "$fileName". Please select a .bin file.', ), ); } if (selection.fileBytes.isEmpty) { return FirmwareFileSelectionResult.failed( FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.emptyFile, message: 'Selected firmware file "$fileName" is empty. Choose a non-empty .bin file.', ), ); } final imageValidationFailure = _validateBootloaderImage( selection.fileBytes, fileName, ); if (imageValidationFailure != null) { return FirmwareFileSelectionResult.failed(imageValidationFailure); } final vectorStackPointer = _readLeU32(selection.fileBytes, 0); final vectorReset = _readLeU32(selection.fileBytes, 4); final sessionId = _normalizeSessionId(_sessionIdGenerator()); final metadata = BootloaderDfuFirmwareMetadata( totalLength: selection.fileBytes.length, crc32: BootloaderDfuProtocol.crc32(selection.fileBytes), appStart: universalShifterDfuAppStart, imageVersion: 0, sessionId: sessionId, flags: universalShifterDfuFlagNone, vectorStackPointer: vectorStackPointer, vectorReset: vectorReset, ); return FirmwareFileSelectionResult.success( BootloaderDfuPreparedFirmware( fileName: fileName, filePath: selection.filePath, fileBytes: selection.fileBytes, metadata: metadata, ), ); } bool _hasBinExtension(String fileName) { return fileName.toLowerCase().endsWith('.bin'); } FirmwareSelectionFailure? _validateBootloaderImage( Uint8List imageBytes, String fileName, ) { if (imageBytes.length < universalShifterDfuMinimumImageLengthBytes) { return FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.imageTooSmall, message: 'Selected firmware file "$fileName" is too small for a bootloader application image. Need at least $universalShifterDfuMinimumImageLengthBytes bytes.', ); } if (imageBytes.length > universalShifterDfuAppSlotSizeBytes) { return FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.imageTooLarge, message: 'Selected firmware file "$fileName" is ${imageBytes.length} bytes, which exceeds the $universalShifterDfuAppSlotSizeBytes byte application slot.', ); } final vectorStackPointer = _readLeU32(imageBytes, 0); final vectorReset = _readLeU32(imageBytes, 4); final resetAddress = vectorReset & ~0x1; final imageEnd = universalShifterDfuAppStart + imageBytes.length; if (vectorStackPointer < universalShifterDfuRamStart || vectorStackPointer > universalShifterDfuRamEnd || (vectorStackPointer & 0x3) != 0) { return FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.invalidVectorTable, message: 'Selected firmware file "$fileName" has an invalid initial stack pointer (0x${vectorStackPointer.toRadixString(16).padLeft(8, '0').toUpperCase()}).', ); } if ((vectorReset & 0x1) == 0 || resetAddress < universalShifterDfuAppStart + 8 || resetAddress >= imageEnd) { return FirmwareSelectionFailure( reason: FirmwareSelectionFailureReason.invalidVectorTable, message: 'Selected firmware file "$fileName" has an invalid reset vector (0x${vectorReset.toRadixString(16).padLeft(8, '0').toUpperCase()}). Ensure the image starts at application address 0x${universalShifterDfuAppStart.toRadixString(16).padLeft(8, '0').toUpperCase()}.', ); } return null; } int _readLeU32(Uint8List bytes, int offset) { return ByteData.sublistView(bytes).getUint32(offset, Endian.little); } static int _normalizeSessionId(int sessionId) { final normalized = sessionId & 0xFF; return normalized == 0 ? 1 : normalized; } static int _randomSessionId() { return Random.secure().nextInt(255) + 1; } }