feat: add bootloader DFU protocol validation

This commit is contained in:
2026-04-29 17:56:32 +02:00
parent eb26c759e8
commit b673c9100d
7 changed files with 534 additions and 86 deletions

View File

@ -2,34 +2,40 @@ 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:abawo_bt_app/service/firmware_file_selection_service.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('FirmwareFileSelectionService', () {
test('prepares v1 metadata for selected .bin firmware', () async {
test('prepares bootloader metadata for selected .bin firmware', () async {
final image = _validBootloaderImage();
final service = FirmwareFileSelectionService(
filePicker: _FakeFirmwareFilePicker(
selection: FirmwarePickerSelection(
fileName: 'firmware.BIN',
filePath: '/tmp/firmware.BIN',
fileBytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
fileBytes: image,
),
),
sessionIdGenerator: () => 0x1AB,
);
final result = await service.selectAndPrepareDfuV1();
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isTrue);
final firmware = result.firmware!;
expect(firmware.fileName, 'firmware.BIN');
expect(firmware.filePath, '/tmp/firmware.BIN');
expect(firmware.fileBytes, <int>[1, 2, 3, 4]);
expect(firmware.metadata.totalLength, 4);
expect(firmware.metadata.crc32, 0xB63CFBCD);
expect(firmware.fileBytes, image);
expect(firmware.metadata.totalLength, image.length);
expect(firmware.metadata.crc32, BootloaderDfuProtocol.crc32(image));
expect(firmware.metadata.appStart, universalShifterDfuAppStart);
expect(firmware.metadata.imageVersion, 0);
expect(firmware.metadata.sessionId, 0xAB);
expect(firmware.metadata.flags, universalShifterDfuFlagNone);
expect(firmware.metadata.vectorStackPointer, 0x20001000);
expect(firmware.metadata.vectorReset, 0x00030009);
});
test('returns canceled result when user dismisses picker', () async {
@ -37,7 +43,7 @@ void main() {
filePicker: _FakeFirmwareFilePicker(selection: null),
);
final result = await service.selectAndPrepareDfuV1();
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isFalse);
expect(result.isCanceled, isTrue);
@ -49,12 +55,12 @@ void main() {
filePicker: _FakeFirmwareFilePicker(
selection: FirmwarePickerSelection(
fileName: 'firmware.hex',
fileBytes: Uint8List.fromList(<int>[1]),
fileBytes: _validBootloaderImage(),
),
),
);
final result = await service.selectAndPrepareDfuV1();
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isFalse);
expect(result.failure?.reason,
@ -71,26 +77,82 @@ void main() {
),
);
final result = await service.selectAndPrepareDfuV1();
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isFalse);
expect(result.failure?.reason, FirmwareSelectionFailureReason.emptyFile);
});
test('rejects images that are too small for a vector table', () async {
final service = FirmwareFileSelectionService(
filePicker: _FakeFirmwareFilePicker(
selection: FirmwarePickerSelection(
fileName: 'firmware.bin',
fileBytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
),
),
);
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isFalse);
expect(
result.failure?.reason, FirmwareSelectionFailureReason.imageTooSmall);
});
test('rejects images larger than the application slot', () async {
final image = Uint8List(universalShifterDfuAppSlotSizeBytes + 1);
_writeLeU32(image, 0, 0x20001000);
_writeLeU32(image, 4, 0x00030009);
final service = FirmwareFileSelectionService(
filePicker: _FakeFirmwareFilePicker(
selection: FirmwarePickerSelection(
fileName: 'firmware.bin',
fileBytes: image,
),
),
);
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isFalse);
expect(
result.failure?.reason, FirmwareSelectionFailureReason.imageTooLarge);
});
test('rejects images with invalid vector table', () async {
final image = _validBootloaderImage();
_writeLeU32(image, 0, 0x10001000);
final service = FirmwareFileSelectionService(
filePicker: _FakeFirmwareFilePicker(
selection: FirmwarePickerSelection(
fileName: 'firmware.bin',
fileBytes: image,
),
),
);
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isFalse);
expect(result.failure?.reason,
FirmwareSelectionFailureReason.invalidVectorTable);
});
test('generates session id per run', () async {
var nextSession = 9;
final service = FirmwareFileSelectionService(
filePicker: _FakeFirmwareFilePicker(
selection: FirmwarePickerSelection(
fileName: 'firmware.bin',
fileBytes: Uint8List.fromList(<int>[10]),
fileBytes: _validBootloaderImage(),
),
),
sessionIdGenerator: () => nextSession++,
);
final first = await service.selectAndPrepareDfuV1();
final second = await service.selectAndPrepareDfuV1();
final first = await service.selectAndPrepareBootloaderDfu();
final second = await service.selectAndPrepareBootloaderDfu();
expect(first.firmware?.metadata.sessionId, 9);
expect(second.firmware?.metadata.sessionId, 10);
@ -104,7 +166,7 @@ void main() {
),
);
final result = await service.selectAndPrepareDfuV1();
final result = await service.selectAndPrepareBootloaderDfu();
expect(result.isSuccess, isFalse);
expect(result.failure?.reason, FirmwareSelectionFailureReason.readFailed);
@ -113,6 +175,18 @@ void main() {
});
}
Uint8List _validBootloaderImage() {
final image = Uint8List(16);
_writeLeU32(image, 0, 0x20001000);
_writeLeU32(image, 4, 0x00030009);
return image;
}
void _writeLeU32(Uint8List bytes, int offset, int value) {
final data = ByteData.sublistView(bytes);
data.setUint32(offset, value, Endian.little);
}
class _FakeFirmwareFilePicker implements FirmwareFilePicker {
_FakeFirmwareFilePicker({
required this.selection,