222 lines
7.0 KiB
Dart
222 lines
7.0 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 metadata = BootloaderDfuFirmwareMetadata(
|
|
totalLength: selection.fileBytes.length,
|
|
crc32: BootloaderDfuProtocol.crc32(selection.fileBytes),
|
|
appStart: universalShifterDfuAppStart,
|
|
imageVersion: 0,
|
|
sessionId: _sessionIdGenerator() & 0xFF,
|
|
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 _randomSessionId() {
|
|
return Random.secure().nextInt(256);
|
|
}
|
|
}
|