feat(dfu): add firmware file selection and validation
This commit is contained in:
70
lib/model/firmware_file_selection.dart
Normal file
70
lib/model/firmware_file_selection.dart
Normal file
@ -0,0 +1,70 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
class DfuV1FirmwareMetadata {
|
||||
const DfuV1FirmwareMetadata({
|
||||
required this.totalLength,
|
||||
required this.crc32,
|
||||
required this.sessionId,
|
||||
required this.flags,
|
||||
});
|
||||
|
||||
final int totalLength;
|
||||
final int crc32;
|
||||
final int sessionId;
|
||||
final int flags;
|
||||
}
|
||||
|
||||
class DfuV1PreparedFirmware {
|
||||
const DfuV1PreparedFirmware({
|
||||
required this.fileName,
|
||||
required this.fileBytes,
|
||||
required this.metadata,
|
||||
this.filePath,
|
||||
});
|
||||
|
||||
final String fileName;
|
||||
final String? filePath;
|
||||
final Uint8List fileBytes;
|
||||
final DfuV1FirmwareMetadata metadata;
|
||||
}
|
||||
|
||||
enum FirmwareSelectionFailureReason {
|
||||
canceled,
|
||||
malformedSelection,
|
||||
unsupportedExtension,
|
||||
emptyFile,
|
||||
readFailed,
|
||||
}
|
||||
|
||||
class FirmwareSelectionFailure {
|
||||
const FirmwareSelectionFailure({
|
||||
required this.reason,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
final FirmwareSelectionFailureReason reason;
|
||||
final String message;
|
||||
}
|
||||
|
||||
class FirmwareFileSelectionResult {
|
||||
const FirmwareFileSelectionResult._({
|
||||
this.firmware,
|
||||
this.failure,
|
||||
});
|
||||
|
||||
final DfuV1PreparedFirmware? firmware;
|
||||
final FirmwareSelectionFailure? failure;
|
||||
|
||||
bool get isSuccess => firmware != null;
|
||||
|
||||
bool get isCanceled =>
|
||||
failure?.reason == FirmwareSelectionFailureReason.canceled;
|
||||
|
||||
static FirmwareFileSelectionResult success(DfuV1PreparedFirmware firmware) {
|
||||
return FirmwareFileSelectionResult._(firmware: firmware);
|
||||
}
|
||||
|
||||
static FirmwareFileSelectionResult failed(FirmwareSelectionFailure failure) {
|
||||
return FirmwareFileSelectionResult._(failure: failure);
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,7 @@ const int universalShifterDfuPreferredMtu = 128;
|
||||
|
||||
const int universalShifterDfuFlagEncrypted = 0x01;
|
||||
const int universalShifterDfuFlagSigned = 0x02;
|
||||
const int universalShifterDfuFlagNone = 0x00;
|
||||
|
||||
const int errorSequence = 1;
|
||||
const int errorFtmsMissing = 2;
|
||||
|
||||
154
lib/service/firmware_file_selection_service.dart
Normal file
154
lib/service/firmware_file_selection_service.dart
Normal file
@ -0,0 +1,154 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user