feat(dfu): add firmware file selection and validation

This commit is contained in:
2026-03-03 17:06:54 +01:00
parent 8b24084f97
commit aafa9928ac
7 changed files with 392 additions and 0 deletions

View 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);
}
}

View File

@ -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;

View 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);
}
}