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 selectAndPrepareDfuV1() 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 metadata = DfuV1FirmwareMetadata( totalLength: selection.fileBytes.length, crc32: DfuProtocol.crc32(selection.fileBytes), sessionId: _sessionIdGenerator() & 0xFF, flags: universalShifterDfuFlagNone, ); return FirmwareFileSelectionResult.success( DfuV1PreparedFirmware( fileName: fileName, filePath: selection.filePath, fileBytes: selection.fileBytes, metadata: metadata, ), ); } bool _hasBinExtension(String fileName) { return fileName.toLowerCase().endsWith('.bin'); } static int _randomSessionId() { return Random.secure().nextInt(256); } }