155 lines
4.3 KiB
Dart
155 lines
4.3 KiB
Dart
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<FirmwarePickerSelection?> pickFirmwareFile();
|
|
}
|
|
|
|
class LocalFirmwareFilePicker implements FirmwareFilePicker {
|
|
@override
|
|
Future<FirmwarePickerSelection?> 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<Uint8List> _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<FirmwareFileSelectionResult> 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);
|
|
}
|
|
}
|