Files
abawo-bt-app/lib/service/firmware_file_selection_service.dart

229 lines
7.2 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> 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;
}
}