feat: add bootloader DFU protocol validation
This commit is contained in:
@ -1,21 +1,29 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
class DfuV1FirmwareMetadata {
|
class BootloaderDfuFirmwareMetadata {
|
||||||
const DfuV1FirmwareMetadata({
|
const BootloaderDfuFirmwareMetadata({
|
||||||
required this.totalLength,
|
required this.totalLength,
|
||||||
required this.crc32,
|
required this.crc32,
|
||||||
|
required this.appStart,
|
||||||
|
required this.imageVersion,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.flags,
|
required this.flags,
|
||||||
|
required this.vectorStackPointer,
|
||||||
|
required this.vectorReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int totalLength;
|
final int totalLength;
|
||||||
final int crc32;
|
final int crc32;
|
||||||
|
final int appStart;
|
||||||
|
final int imageVersion;
|
||||||
final int sessionId;
|
final int sessionId;
|
||||||
final int flags;
|
final int flags;
|
||||||
|
final int vectorStackPointer;
|
||||||
|
final int vectorReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DfuV1PreparedFirmware {
|
class BootloaderDfuPreparedFirmware {
|
||||||
const DfuV1PreparedFirmware({
|
const BootloaderDfuPreparedFirmware({
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
required this.fileBytes,
|
required this.fileBytes,
|
||||||
required this.metadata,
|
required this.metadata,
|
||||||
@ -25,7 +33,7 @@ class DfuV1PreparedFirmware {
|
|||||||
final String fileName;
|
final String fileName;
|
||||||
final String? filePath;
|
final String? filePath;
|
||||||
final Uint8List fileBytes;
|
final Uint8List fileBytes;
|
||||||
final DfuV1FirmwareMetadata metadata;
|
final BootloaderDfuFirmwareMetadata metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FirmwareSelectionFailureReason {
|
enum FirmwareSelectionFailureReason {
|
||||||
@ -33,6 +41,9 @@ enum FirmwareSelectionFailureReason {
|
|||||||
malformedSelection,
|
malformedSelection,
|
||||||
unsupportedExtension,
|
unsupportedExtension,
|
||||||
emptyFile,
|
emptyFile,
|
||||||
|
imageTooSmall,
|
||||||
|
imageTooLarge,
|
||||||
|
invalidVectorTable,
|
||||||
readFailed,
|
readFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +63,7 @@ class FirmwareFileSelectionResult {
|
|||||||
this.failure,
|
this.failure,
|
||||||
});
|
});
|
||||||
|
|
||||||
final DfuV1PreparedFirmware? firmware;
|
final BootloaderDfuPreparedFirmware? firmware;
|
||||||
final FirmwareSelectionFailure? failure;
|
final FirmwareSelectionFailure? failure;
|
||||||
|
|
||||||
bool get isSuccess => firmware != null;
|
bool get isSuccess => firmware != null;
|
||||||
@ -60,7 +71,8 @@ class FirmwareFileSelectionResult {
|
|||||||
bool get isCanceled =>
|
bool get isCanceled =>
|
||||||
failure?.reason == FirmwareSelectionFailureReason.canceled;
|
failure?.reason == FirmwareSelectionFailureReason.canceled;
|
||||||
|
|
||||||
static FirmwareFileSelectionResult success(DfuV1PreparedFirmware firmware) {
|
static FirmwareFileSelectionResult success(
|
||||||
|
BootloaderDfuPreparedFirmware firmware) {
|
||||||
return FirmwareFileSelectionResult._(firmware: firmware);
|
return FirmwareFileSelectionResult._(firmware: firmware);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,8 @@ const String universalShifterDfuDataCharacteristicUuid =
|
|||||||
'0993826f-0ee4-4b37-9614-d13ecba40009';
|
'0993826f-0ee4-4b37-9614-d13ecba40009';
|
||||||
const String universalShifterDfuAckCharacteristicUuid =
|
const String universalShifterDfuAckCharacteristicUuid =
|
||||||
'0993826f-0ee4-4b37-9614-d13ecba4000a';
|
'0993826f-0ee4-4b37-9614-d13ecba4000a';
|
||||||
|
const String universalShifterDfuStatusCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4000a';
|
||||||
const String ftmsServiceUuid = '00001826-0000-1000-8000-00805f9b34fb';
|
const String ftmsServiceUuid = '00001826-0000-1000-8000-00805f9b34fb';
|
||||||
const String batteryServiceUuid = '0000180f-0000-1000-8000-00805f9b34fb';
|
const String batteryServiceUuid = '0000180f-0000-1000-8000-00805f9b34fb';
|
||||||
const String batteryLevelCharacteristicUuid =
|
const String batteryLevelCharacteristicUuid =
|
||||||
@ -36,14 +38,26 @@ bool isFtmsUuid(Uuid uuid) {
|
|||||||
const int universalShifterDfuOpcodeStart = 0x01;
|
const int universalShifterDfuOpcodeStart = 0x01;
|
||||||
const int universalShifterDfuOpcodeFinish = 0x02;
|
const int universalShifterDfuOpcodeFinish = 0x02;
|
||||||
const int universalShifterDfuOpcodeAbort = 0x03;
|
const int universalShifterDfuOpcodeAbort = 0x03;
|
||||||
|
const int universalShifterDfuOpcodeGetStatus = 0x04;
|
||||||
|
|
||||||
const int universalShifterDfuFrameSizeBytes = 64;
|
const int universalShifterDfuFrameSizeBytes = 64;
|
||||||
const int universalShifterDfuFramePayloadSizeBytes = 63;
|
const int universalShifterDfuFramePayloadSizeBytes = 63;
|
||||||
|
const int universalShifterBootloaderDfuDataHeaderSizeBytes = 9;
|
||||||
|
const int universalShifterBootloaderDfuMaxPayloadSizeBytes =
|
||||||
|
universalShifterDfuFrameSizeBytes -
|
||||||
|
universalShifterBootloaderDfuDataHeaderSizeBytes;
|
||||||
|
const int universalShifterBootloaderDfuStatusSizeBytes = 6;
|
||||||
const int universalShifterAttWriteOverheadBytes = 3;
|
const int universalShifterAttWriteOverheadBytes = 3;
|
||||||
const int universalShifterDfuMinimumMtu =
|
const int universalShifterDfuMinimumMtu =
|
||||||
universalShifterDfuFrameSizeBytes + universalShifterAttWriteOverheadBytes;
|
universalShifterDfuFrameSizeBytes + universalShifterAttWriteOverheadBytes;
|
||||||
const int universalShifterDfuPreferredMtu = 128;
|
const int universalShifterDfuPreferredMtu = 128;
|
||||||
|
|
||||||
|
const int universalShifterDfuAppStart = 0x00030000;
|
||||||
|
const int universalShifterDfuAppSlotSizeBytes = 0x00042000;
|
||||||
|
const int universalShifterDfuMinimumImageLengthBytes = 8;
|
||||||
|
const int universalShifterDfuRamStart = 0x20000000;
|
||||||
|
const int universalShifterDfuRamEnd = 0x20010000;
|
||||||
|
|
||||||
const int universalShifterDfuFlagEncrypted = 0x01;
|
const int universalShifterDfuFlagEncrypted = 0x01;
|
||||||
const int universalShifterDfuFlagSigned = 0x02;
|
const int universalShifterDfuFlagSigned = 0x02;
|
||||||
const int universalShifterDfuFlagNone = 0x00;
|
const int universalShifterDfuFlagNone = 0x00;
|
||||||
@ -134,6 +148,49 @@ class DfuUpdateProgress {
|
|||||||
state == DfuUpdateState.failed;
|
state == DfuUpdateState.failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DfuBootloaderStatusCode {
|
||||||
|
ok(0x00),
|
||||||
|
parseError(0x01),
|
||||||
|
stateError(0x02),
|
||||||
|
boundsError(0x03),
|
||||||
|
crcError(0x04),
|
||||||
|
flashError(0x05),
|
||||||
|
unsupportedError(0x06),
|
||||||
|
vectorError(0x07),
|
||||||
|
queueFull(0x08),
|
||||||
|
bootMetadataError(0x09),
|
||||||
|
unknown(-1);
|
||||||
|
|
||||||
|
const DfuBootloaderStatusCode(this.value);
|
||||||
|
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
static DfuBootloaderStatusCode fromRaw(int value) {
|
||||||
|
for (final code in values) {
|
||||||
|
if (code.value == value) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DfuBootloaderStatusCode.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuBootloaderStatus {
|
||||||
|
const DfuBootloaderStatus({
|
||||||
|
required this.code,
|
||||||
|
required this.rawCode,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.expectedOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DfuBootloaderStatusCode code;
|
||||||
|
final int rawCode;
|
||||||
|
final int sessionId;
|
||||||
|
final int expectedOffset;
|
||||||
|
|
||||||
|
bool get isOk => code == DfuBootloaderStatusCode.ok;
|
||||||
|
}
|
||||||
|
|
||||||
enum DfuPreflightFailureReason {
|
enum DfuPreflightFailureReason {
|
||||||
deviceNotConnected,
|
deviceNotConnected,
|
||||||
wrongConnectedDevice,
|
wrongConnectedDevice,
|
||||||
@ -253,7 +310,8 @@ enum UniversalShifterCommand {
|
|||||||
stopScan(0x02),
|
stopScan(0x02),
|
||||||
connectToDevice(0x03),
|
connectToDevice(0x03),
|
||||||
disconnect(0x04),
|
disconnect(0x04),
|
||||||
turnOff(0x05);
|
turnOff(0x05),
|
||||||
|
enterDfu(0x06);
|
||||||
|
|
||||||
const UniversalShifterCommand(this.value);
|
const UniversalShifterCommand(this.value);
|
||||||
final int value;
|
final int value;
|
||||||
|
|||||||
@ -80,7 +80,7 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
late final FirmwareFileSelectionService _firmwareFileSelectionService;
|
late final FirmwareFileSelectionService _firmwareFileSelectionService;
|
||||||
FirmwareUpdateService? _firmwareUpdateService;
|
FirmwareUpdateService? _firmwareUpdateService;
|
||||||
StreamSubscription<DfuUpdateProgress>? _firmwareProgressSubscription;
|
StreamSubscription<DfuUpdateProgress>? _firmwareProgressSubscription;
|
||||||
DfuV1PreparedFirmware? _selectedFirmware;
|
BootloaderDfuPreparedFirmware? _selectedFirmware;
|
||||||
DfuUpdateProgress _dfuProgress = const DfuUpdateProgress(
|
DfuUpdateProgress _dfuProgress = const DfuUpdateProgress(
|
||||||
state: DfuUpdateState.idle,
|
state: DfuUpdateState.idle,
|
||||||
totalBytes: 0,
|
totalBytes: 0,
|
||||||
@ -548,7 +548,8 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
_firmwareUserMessage = null;
|
_firmwareUserMessage = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
final result = await _firmwareFileSelectionService.selectAndPrepareDfuV1();
|
final result =
|
||||||
|
await _firmwareFileSelectionService.selectAndPrepareBootloaderDfu();
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1061,7 +1062,7 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
|||||||
required this.onStartUpdate,
|
required this.onStartUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
final DfuV1PreparedFirmware? selectedFirmware;
|
final BootloaderDfuPreparedFirmware? selectedFirmware;
|
||||||
final DfuUpdateProgress progress;
|
final DfuUpdateProgress progress;
|
||||||
final bool isSelecting;
|
final bool isSelecting;
|
||||||
final bool isStarting;
|
final bool isStarting;
|
||||||
|
|||||||
@ -3,6 +3,39 @@ import 'dart:typed_data';
|
|||||||
import 'package:abawo_bt_app/model/shifter_types.dart';
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
|
||||||
const int _startPayloadLength = 11;
|
const int _startPayloadLength = 11;
|
||||||
|
const int _bootloaderStartPayloadLength = 19;
|
||||||
|
|
||||||
|
class BootloaderDfuStartPayload {
|
||||||
|
const BootloaderDfuStartPayload({
|
||||||
|
required this.totalLength,
|
||||||
|
required this.imageCrc32,
|
||||||
|
this.appStart = universalShifterDfuAppStart,
|
||||||
|
this.imageVersion = 0,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.flags,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int totalLength;
|
||||||
|
final int imageCrc32;
|
||||||
|
final int appStart;
|
||||||
|
final int imageVersion;
|
||||||
|
final int sessionId;
|
||||||
|
final int flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BootloaderDfuDataFrame {
|
||||||
|
const BootloaderDfuDataFrame({
|
||||||
|
required this.sessionId,
|
||||||
|
required this.offset,
|
||||||
|
required this.payloadLength,
|
||||||
|
required this.bytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int sessionId;
|
||||||
|
final int offset;
|
||||||
|
final int payloadLength;
|
||||||
|
final Uint8List bytes;
|
||||||
|
}
|
||||||
|
|
||||||
class DfuStartPayload {
|
class DfuStartPayload {
|
||||||
const DfuStartPayload({
|
const DfuStartPayload({
|
||||||
@ -135,3 +168,145 @@ class DfuProtocol {
|
|||||||
return value & 0xFF;
|
return value & 0xFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BootloaderDfuProtocol {
|
||||||
|
const BootloaderDfuProtocol._();
|
||||||
|
|
||||||
|
static Uint8List encodeStartPayload(BootloaderDfuStartPayload payload) {
|
||||||
|
final data = ByteData(_bootloaderStartPayloadLength);
|
||||||
|
data.setUint8(0, universalShifterDfuOpcodeStart);
|
||||||
|
data.setUint32(1, payload.totalLength, Endian.little);
|
||||||
|
data.setUint32(5, payload.imageCrc32, Endian.little);
|
||||||
|
data.setUint32(9, payload.appStart, Endian.little);
|
||||||
|
data.setUint32(13, payload.imageVersion, Endian.little);
|
||||||
|
data.setUint8(17, payload.sessionId & 0xFF);
|
||||||
|
data.setUint8(18, payload.flags & 0xFF);
|
||||||
|
return data.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List encodeFinishPayload(int sessionId) {
|
||||||
|
return Uint8List.fromList([
|
||||||
|
universalShifterDfuOpcodeFinish,
|
||||||
|
sessionId & 0xFF,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List encodeAbortPayload(int sessionId) {
|
||||||
|
return Uint8List.fromList([
|
||||||
|
universalShifterDfuOpcodeAbort,
|
||||||
|
sessionId & 0xFF,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List encodeGetStatusPayload() {
|
||||||
|
return Uint8List.fromList([universalShifterDfuOpcodeGetStatus]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BootloaderDfuDataFrame buildDataFrame({
|
||||||
|
required List<int> imageBytes,
|
||||||
|
required int sessionId,
|
||||||
|
required int offset,
|
||||||
|
int payloadSize = universalShifterBootloaderDfuMaxPayloadSizeBytes,
|
||||||
|
}) {
|
||||||
|
if (offset < 0 || offset >= imageBytes.length) {
|
||||||
|
throw RangeError.range(offset, 0, imageBytes.length - 1, 'offset');
|
||||||
|
}
|
||||||
|
if (payloadSize <= 0 ||
|
||||||
|
payloadSize > universalShifterBootloaderDfuMaxPayloadSizeBytes) {
|
||||||
|
throw RangeError.range(
|
||||||
|
payloadSize,
|
||||||
|
1,
|
||||||
|
universalShifterBootloaderDfuMaxPayloadSizeBytes,
|
||||||
|
'payloadSize',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final remaining = imageBytes.length - offset;
|
||||||
|
final payloadLength = remaining < payloadSize ? remaining : payloadSize;
|
||||||
|
final payloadEnd = offset + payloadLength;
|
||||||
|
final payload = imageBytes.sublist(offset, payloadEnd);
|
||||||
|
final frame = Uint8List(
|
||||||
|
universalShifterBootloaderDfuDataHeaderSizeBytes + payloadLength,
|
||||||
|
);
|
||||||
|
|
||||||
|
frame[0] = sessionId & 0xFF;
|
||||||
|
final data = ByteData.view(frame.buffer);
|
||||||
|
data.setUint32(1, offset, Endian.little);
|
||||||
|
data.setUint32(5, crc32(payload), Endian.little);
|
||||||
|
frame.setRange(
|
||||||
|
universalShifterBootloaderDfuDataHeaderSizeBytes,
|
||||||
|
universalShifterBootloaderDfuDataHeaderSizeBytes + payloadLength,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
return BootloaderDfuDataFrame(
|
||||||
|
sessionId: sessionId & 0xFF,
|
||||||
|
offset: offset,
|
||||||
|
payloadLength: payloadLength,
|
||||||
|
bytes: frame,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<BootloaderDfuDataFrame> buildDataFrames({
|
||||||
|
required List<int> imageBytes,
|
||||||
|
required int sessionId,
|
||||||
|
int payloadSize = universalShifterBootloaderDfuMaxPayloadSizeBytes,
|
||||||
|
}) {
|
||||||
|
final frames = <BootloaderDfuDataFrame>[];
|
||||||
|
var offset = 0;
|
||||||
|
while (offset < imageBytes.length) {
|
||||||
|
final frame = buildDataFrame(
|
||||||
|
imageBytes: imageBytes,
|
||||||
|
sessionId: sessionId,
|
||||||
|
offset: offset,
|
||||||
|
payloadSize: payloadSize,
|
||||||
|
);
|
||||||
|
frames.add(frame);
|
||||||
|
offset += frame.payloadLength;
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int maxPayloadSizeForMtu(int negotiatedMtu) {
|
||||||
|
final writePayloadBytes =
|
||||||
|
negotiatedMtu - universalShifterAttWriteOverheadBytes;
|
||||||
|
final availablePayload =
|
||||||
|
writePayloadBytes - universalShifterBootloaderDfuDataHeaderSizeBytes;
|
||||||
|
if (availablePayload <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (availablePayload > universalShifterBootloaderDfuMaxPayloadSizeBytes) {
|
||||||
|
return universalShifterBootloaderDfuMaxPayloadSizeBytes;
|
||||||
|
}
|
||||||
|
return availablePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DfuBootloaderStatus parseStatusPayload(List<int> payload) {
|
||||||
|
if (payload.length != universalShifterBootloaderDfuStatusSizeBytes) {
|
||||||
|
throw const FormatException(
|
||||||
|
'DFU status payload must be exactly 6 bytes.');
|
||||||
|
}
|
||||||
|
final data = ByteData.sublistView(Uint8List.fromList(payload));
|
||||||
|
final rawCode = data.getUint8(0);
|
||||||
|
return DfuBootloaderStatus(
|
||||||
|
code: DfuBootloaderStatusCode.fromRaw(rawCode),
|
||||||
|
rawCode: rawCode,
|
||||||
|
sessionId: data.getUint8(1),
|
||||||
|
expectedOffset: data.getUint32(2, Endian.little),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const int crc32Initial = DfuProtocol.crc32Initial;
|
||||||
|
|
||||||
|
static int crc32Update(int crc, List<int> bytes) {
|
||||||
|
return DfuProtocol.crc32Update(crc, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int crc32Finalize(int crc) {
|
||||||
|
return DfuProtocol.crc32Finalize(crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int crc32(List<int> bytes) {
|
||||||
|
return DfuProtocol.crc32(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -75,7 +75,7 @@ class FirmwareFileSelectionService {
|
|||||||
final FirmwareFilePicker _filePicker;
|
final FirmwareFilePicker _filePicker;
|
||||||
final SessionIdGenerator _sessionIdGenerator;
|
final SessionIdGenerator _sessionIdGenerator;
|
||||||
|
|
||||||
Future<FirmwareFileSelectionResult> selectAndPrepareDfuV1() async {
|
Future<FirmwareFileSelectionResult> selectAndPrepareBootloaderDfu() async {
|
||||||
final FirmwarePickerSelection? selection;
|
final FirmwarePickerSelection? selection;
|
||||||
try {
|
try {
|
||||||
selection = await _filePicker.pickFirmwareFile();
|
selection = await _filePicker.pickFirmwareFile();
|
||||||
@ -127,15 +127,30 @@ class FirmwareFileSelectionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final metadata = DfuV1FirmwareMetadata(
|
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,
|
totalLength: selection.fileBytes.length,
|
||||||
crc32: DfuProtocol.crc32(selection.fileBytes),
|
crc32: BootloaderDfuProtocol.crc32(selection.fileBytes),
|
||||||
|
appStart: universalShifterDfuAppStart,
|
||||||
|
imageVersion: 0,
|
||||||
sessionId: _sessionIdGenerator() & 0xFF,
|
sessionId: _sessionIdGenerator() & 0xFF,
|
||||||
flags: universalShifterDfuFlagNone,
|
flags: universalShifterDfuFlagNone,
|
||||||
|
vectorStackPointer: vectorStackPointer,
|
||||||
|
vectorReset: vectorReset,
|
||||||
);
|
);
|
||||||
|
|
||||||
return FirmwareFileSelectionResult.success(
|
return FirmwareFileSelectionResult.success(
|
||||||
DfuV1PreparedFirmware(
|
BootloaderDfuPreparedFirmware(
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
filePath: selection.filePath,
|
filePath: selection.filePath,
|
||||||
fileBytes: selection.fileBytes,
|
fileBytes: selection.fileBytes,
|
||||||
@ -148,6 +163,58 @@ class FirmwareFileSelectionService {
|
|||||||
return fileName.toLowerCase().endsWith('.bin');
|
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() {
|
static int _randomSessionId() {
|
||||||
return Random.secure().nextInt(256);
|
return Random.secure().nextInt(256);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,97 +3,158 @@ import 'package:abawo_bt_app/service/dfu_protocol.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('DfuProtocol CRC32', () {
|
group('BootloaderDfuProtocol CRC32', () {
|
||||||
test('matches known vector', () {
|
test('matches known vector', () {
|
||||||
final crc = DfuProtocol.crc32('123456789'.codeUnits);
|
final crc = BootloaderDfuProtocol.crc32('123456789'.codeUnits);
|
||||||
expect(crc, 0xCBF43926);
|
expect(crc, 0xCBF43926);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('DfuProtocol control payload encoding', () {
|
group('BootloaderDfuProtocol control payload encoding', () {
|
||||||
test('encodes START payload with exact 11-byte LE layout', () {
|
test('encodes START payload with exact 19-byte LE layout', () {
|
||||||
final payload = DfuProtocol.encodeStartPayload(
|
final payload = BootloaderDfuProtocol.encodeStartPayload(
|
||||||
const DfuStartPayload(
|
const BootloaderDfuStartPayload(
|
||||||
totalLength: 0x1234,
|
totalLength: 0x1234,
|
||||||
imageCrc32: 0x89ABCDEF,
|
imageCrc32: 0x89ABCDEF,
|
||||||
|
appStart: universalShifterDfuAppStart,
|
||||||
|
imageVersion: 0x10203040,
|
||||||
sessionId: 0x22,
|
sessionId: 0x22,
|
||||||
flags: universalShifterDfuFlagEncrypted,
|
flags: universalShifterDfuFlagNone,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(payload.length, 11);
|
expect(payload.length, 19);
|
||||||
expect(
|
expect(payload, [
|
||||||
payload,
|
universalShifterDfuOpcodeStart,
|
||||||
[
|
0x34,
|
||||||
universalShifterDfuOpcodeStart,
|
0x12,
|
||||||
0x34,
|
0x00,
|
||||||
0x12,
|
0x00,
|
||||||
0x00,
|
0xEF,
|
||||||
0x00,
|
0xCD,
|
||||||
0xEF,
|
0xAB,
|
||||||
0xCD,
|
0x89,
|
||||||
0xAB,
|
0x00,
|
||||||
0x89,
|
0x00,
|
||||||
0x22,
|
0x03,
|
||||||
universalShifterDfuFlagEncrypted,
|
0x00,
|
||||||
],
|
0x40,
|
||||||
);
|
0x30,
|
||||||
|
0x20,
|
||||||
|
0x10,
|
||||||
|
0x22,
|
||||||
|
universalShifterDfuFlagNone,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('encodes FINISH and ABORT payloads as one byte', () {
|
test('encodes FINISH, ABORT, and GET_STATUS payloads', () {
|
||||||
expect(
|
expect(
|
||||||
DfuProtocol.encodeFinishPayload(), [universalShifterDfuOpcodeFinish]);
|
BootloaderDfuProtocol.encodeFinishPayload(0x12),
|
||||||
|
[universalShifterDfuOpcodeFinish, 0x12],
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
DfuProtocol.encodeAbortPayload(), [universalShifterDfuOpcodeAbort]);
|
BootloaderDfuProtocol.encodeAbortPayload(0x34),
|
||||||
|
[universalShifterDfuOpcodeAbort, 0x34],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
BootloaderDfuProtocol.encodeGetStatusPayload(),
|
||||||
|
[universalShifterDfuOpcodeGetStatus],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('DfuProtocol data frame building', () {
|
group('BootloaderDfuProtocol data frame building', () {
|
||||||
test('builds 64-byte frames and handles final partial payload', () {
|
test('builds offset frames with payload CRC and variable final length', () {
|
||||||
final image = List<int>.generate(80, (index) => index);
|
final image = List<int>.generate(60, (index) => index);
|
||||||
final frames = DfuProtocol.buildDataFrames(image);
|
final frames = BootloaderDfuProtocol.buildDataFrames(
|
||||||
|
imageBytes: image,
|
||||||
|
sessionId: 0x7A,
|
||||||
|
);
|
||||||
|
|
||||||
expect(frames.length, 2);
|
expect(frames.length, 2);
|
||||||
|
|
||||||
expect(frames[0].sequence, 0);
|
expect(frames[0].sessionId, 0x7A);
|
||||||
expect(frames[0].offset, 0);
|
expect(frames[0].offset, 0);
|
||||||
expect(frames[0].payloadLength, universalShifterDfuFramePayloadSizeBytes);
|
expect(frames[0].payloadLength,
|
||||||
|
universalShifterBootloaderDfuMaxPayloadSizeBytes);
|
||||||
expect(frames[0].bytes.length, universalShifterDfuFrameSizeBytes);
|
expect(frames[0].bytes.length, universalShifterDfuFrameSizeBytes);
|
||||||
expect(frames[0].bytes.sublist(1, 64), image.sublist(0, 63));
|
expect(frames[0].bytes[0], 0x7A);
|
||||||
|
expect(frames[0].bytes.sublist(1, 5), [0, 0, 0, 0]);
|
||||||
|
expect(
|
||||||
|
frames[0].bytes.sublist(5, 9),
|
||||||
|
_leU32Bytes(BootloaderDfuProtocol.crc32(image.sublist(0, 55))),
|
||||||
|
);
|
||||||
|
expect(frames[0].bytes.sublist(9), image.sublist(0, 55));
|
||||||
|
|
||||||
expect(frames[1].sequence, 1);
|
expect(frames[1].offset, 55);
|
||||||
expect(frames[1].offset, 63);
|
expect(frames[1].payloadLength, 5);
|
||||||
expect(frames[1].payloadLength, 17);
|
expect(frames[1].bytes.length, 14);
|
||||||
expect(frames[1].bytes.length, universalShifterDfuFrameSizeBytes);
|
expect(frames[1].bytes.sublist(1, 5), [55, 0, 0, 0]);
|
||||||
expect(frames[1].bytes.sublist(1, 18), image.sublist(63, 80));
|
expect(frames[1].bytes.sublist(9), image.sublist(55));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uses deterministic wrapping sequence numbers from custom start', () {
|
test('uses caller supplied payload size for low-MTU links', () {
|
||||||
final image = List<int>.generate(
|
final image = List<int>.generate(15, (index) => index);
|
||||||
3 * universalShifterDfuFramePayloadSizeBytes,
|
final frames = BootloaderDfuProtocol.buildDataFrames(
|
||||||
(index) => index & 0xFF);
|
imageBytes: image,
|
||||||
|
sessionId: 0x01,
|
||||||
|
payloadSize: 4,
|
||||||
|
);
|
||||||
|
|
||||||
final frames = DfuProtocol.buildDataFrames(image, startSequence: 0xFE);
|
expect(frames.map((frame) => frame.payloadLength), [4, 4, 4, 3]);
|
||||||
|
expect(frames.map((frame) => frame.offset), [0, 4, 8, 12]);
|
||||||
|
});
|
||||||
|
|
||||||
expect(frames.length, 3);
|
test('calculates safe payload size from negotiated MTU', () {
|
||||||
expect(frames[0].sequence, 0xFE);
|
expect(
|
||||||
expect(frames[1].sequence, 0xFF);
|
BootloaderDfuProtocol.maxPayloadSizeForMtu(64),
|
||||||
expect(frames[2].sequence, 0x00);
|
universalShifterBootloaderDfuMaxPayloadSizeBytes - 3,
|
||||||
|
);
|
||||||
|
expect(BootloaderDfuProtocol.maxPayloadSizeForMtu(23), 11);
|
||||||
|
expect(BootloaderDfuProtocol.maxPayloadSizeForMtu(12), 0);
|
||||||
|
expect(
|
||||||
|
BootloaderDfuProtocol.maxPayloadSizeForMtu(128),
|
||||||
|
universalShifterBootloaderDfuMaxPayloadSizeBytes,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('DfuProtocol sequence and ACK helpers', () {
|
group('BootloaderDfuProtocol status parsing', () {
|
||||||
test('wraps sequence values and computes ack+1 rewind', () {
|
test('parses bootloader status payload', () {
|
||||||
expect(DfuProtocol.nextSequence(0x00), 0x01);
|
final status = BootloaderDfuProtocol.parseStatusPayload(
|
||||||
expect(DfuProtocol.nextSequence(0xFF), 0x00);
|
[0x00, 0x22, 0x78, 0x56, 0x34, 0x12],
|
||||||
|
);
|
||||||
|
|
||||||
expect(DfuProtocol.rewindSequenceFromAck(0x05), 0x06);
|
expect(status.code, DfuBootloaderStatusCode.ok);
|
||||||
expect(DfuProtocol.rewindSequenceFromAck(0xFF), 0x00);
|
expect(status.rawCode, 0x00);
|
||||||
|
expect(status.sessionId, 0x22);
|
||||||
|
expect(status.expectedOffset, 0x12345678);
|
||||||
|
expect(status.isOk, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('computes wrapping sequence distance', () {
|
test('preserves unknown status codes', () {
|
||||||
expect(DfuProtocol.sequenceDistance(250, 2), 8);
|
final status = BootloaderDfuProtocol.parseStatusPayload(
|
||||||
expect(DfuProtocol.sequenceDistance(1, 1), 0);
|
[0xFE, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(status.code, DfuBootloaderStatusCode.unknown);
|
||||||
|
expect(status.rawCode, 0xFE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects malformed status payloads', () {
|
||||||
|
expect(
|
||||||
|
() => BootloaderDfuProtocol.parseStatusPayload(const [0, 1]),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<int> _leU32Bytes(int value) {
|
||||||
|
return [
|
||||||
|
value & 0xFF,
|
||||||
|
(value >> 8) & 0xFF,
|
||||||
|
(value >> 16) & 0xFF,
|
||||||
|
(value >> 24) & 0xFF,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@ -2,34 +2,40 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:abawo_bt_app/model/firmware_file_selection.dart';
|
import 'package:abawo_bt_app/model/firmware_file_selection.dart';
|
||||||
import 'package:abawo_bt_app/model/shifter_types.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:abawo_bt_app/service/firmware_file_selection_service.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('FirmwareFileSelectionService', () {
|
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(
|
final service = FirmwareFileSelectionService(
|
||||||
filePicker: _FakeFirmwareFilePicker(
|
filePicker: _FakeFirmwareFilePicker(
|
||||||
selection: FirmwarePickerSelection(
|
selection: FirmwarePickerSelection(
|
||||||
fileName: 'firmware.BIN',
|
fileName: 'firmware.BIN',
|
||||||
filePath: '/tmp/firmware.BIN',
|
filePath: '/tmp/firmware.BIN',
|
||||||
fileBytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
|
fileBytes: image,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
sessionIdGenerator: () => 0x1AB,
|
sessionIdGenerator: () => 0x1AB,
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await service.selectAndPrepareDfuV1();
|
final result = await service.selectAndPrepareBootloaderDfu();
|
||||||
expect(result.isSuccess, isTrue);
|
expect(result.isSuccess, isTrue);
|
||||||
|
|
||||||
final firmware = result.firmware!;
|
final firmware = result.firmware!;
|
||||||
expect(firmware.fileName, 'firmware.BIN');
|
expect(firmware.fileName, 'firmware.BIN');
|
||||||
expect(firmware.filePath, '/tmp/firmware.BIN');
|
expect(firmware.filePath, '/tmp/firmware.BIN');
|
||||||
expect(firmware.fileBytes, <int>[1, 2, 3, 4]);
|
expect(firmware.fileBytes, image);
|
||||||
expect(firmware.metadata.totalLength, 4);
|
expect(firmware.metadata.totalLength, image.length);
|
||||||
expect(firmware.metadata.crc32, 0xB63CFBCD);
|
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.sessionId, 0xAB);
|
||||||
expect(firmware.metadata.flags, universalShifterDfuFlagNone);
|
expect(firmware.metadata.flags, universalShifterDfuFlagNone);
|
||||||
|
expect(firmware.metadata.vectorStackPointer, 0x20001000);
|
||||||
|
expect(firmware.metadata.vectorReset, 0x00030009);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns canceled result when user dismisses picker', () async {
|
test('returns canceled result when user dismisses picker', () async {
|
||||||
@ -37,7 +43,7 @@ void main() {
|
|||||||
filePicker: _FakeFirmwareFilePicker(selection: null),
|
filePicker: _FakeFirmwareFilePicker(selection: null),
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await service.selectAndPrepareDfuV1();
|
final result = await service.selectAndPrepareBootloaderDfu();
|
||||||
|
|
||||||
expect(result.isSuccess, isFalse);
|
expect(result.isSuccess, isFalse);
|
||||||
expect(result.isCanceled, isTrue);
|
expect(result.isCanceled, isTrue);
|
||||||
@ -49,12 +55,12 @@ void main() {
|
|||||||
filePicker: _FakeFirmwareFilePicker(
|
filePicker: _FakeFirmwareFilePicker(
|
||||||
selection: FirmwarePickerSelection(
|
selection: FirmwarePickerSelection(
|
||||||
fileName: 'firmware.hex',
|
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.isSuccess, isFalse);
|
||||||
expect(result.failure?.reason,
|
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.isSuccess, isFalse);
|
||||||
expect(result.failure?.reason, FirmwareSelectionFailureReason.emptyFile);
|
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 {
|
test('generates session id per run', () async {
|
||||||
var nextSession = 9;
|
var nextSession = 9;
|
||||||
final service = FirmwareFileSelectionService(
|
final service = FirmwareFileSelectionService(
|
||||||
filePicker: _FakeFirmwareFilePicker(
|
filePicker: _FakeFirmwareFilePicker(
|
||||||
selection: FirmwarePickerSelection(
|
selection: FirmwarePickerSelection(
|
||||||
fileName: 'firmware.bin',
|
fileName: 'firmware.bin',
|
||||||
fileBytes: Uint8List.fromList(<int>[10]),
|
fileBytes: _validBootloaderImage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
sessionIdGenerator: () => nextSession++,
|
sessionIdGenerator: () => nextSession++,
|
||||||
);
|
);
|
||||||
|
|
||||||
final first = await service.selectAndPrepareDfuV1();
|
final first = await service.selectAndPrepareBootloaderDfu();
|
||||||
final second = await service.selectAndPrepareDfuV1();
|
final second = await service.selectAndPrepareBootloaderDfu();
|
||||||
|
|
||||||
expect(first.firmware?.metadata.sessionId, 9);
|
expect(first.firmware?.metadata.sessionId, 9);
|
||||||
expect(second.firmware?.metadata.sessionId, 10);
|
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.isSuccess, isFalse);
|
||||||
expect(result.failure?.reason, FirmwareSelectionFailureReason.readFailed);
|
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 {
|
class _FakeFirmwareFilePicker implements FirmwareFilePicker {
|
||||||
_FakeFirmwareFilePicker({
|
_FakeFirmwareFilePicker({
|
||||||
required this.selection,
|
required this.selection,
|
||||||
|
|||||||
Reference in New Issue
Block a user