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

@ -3,97 +3,158 @@ import 'package:abawo_bt_app/service/dfu_protocol.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('DfuProtocol CRC32', () {
group('BootloaderDfuProtocol CRC32', () {
test('matches known vector', () {
final crc = DfuProtocol.crc32('123456789'.codeUnits);
final crc = BootloaderDfuProtocol.crc32('123456789'.codeUnits);
expect(crc, 0xCBF43926);
});
});
group('DfuProtocol control payload encoding', () {
test('encodes START payload with exact 11-byte LE layout', () {
final payload = DfuProtocol.encodeStartPayload(
const DfuStartPayload(
group('BootloaderDfuProtocol control payload encoding', () {
test('encodes START payload with exact 19-byte LE layout', () {
final payload = BootloaderDfuProtocol.encodeStartPayload(
const BootloaderDfuStartPayload(
totalLength: 0x1234,
imageCrc32: 0x89ABCDEF,
appStart: universalShifterDfuAppStart,
imageVersion: 0x10203040,
sessionId: 0x22,
flags: universalShifterDfuFlagEncrypted,
flags: universalShifterDfuFlagNone,
),
);
expect(payload.length, 11);
expect(
payload,
[
universalShifterDfuOpcodeStart,
0x34,
0x12,
0x00,
0x00,
0xEF,
0xCD,
0xAB,
0x89,
0x22,
universalShifterDfuFlagEncrypted,
],
);
expect(payload.length, 19);
expect(payload, [
universalShifterDfuOpcodeStart,
0x34,
0x12,
0x00,
0x00,
0xEF,
0xCD,
0xAB,
0x89,
0x00,
0x00,
0x03,
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(
DfuProtocol.encodeFinishPayload(), [universalShifterDfuOpcodeFinish]);
BootloaderDfuProtocol.encodeFinishPayload(0x12),
[universalShifterDfuOpcodeFinish, 0x12],
);
expect(
DfuProtocol.encodeAbortPayload(), [universalShifterDfuOpcodeAbort]);
BootloaderDfuProtocol.encodeAbortPayload(0x34),
[universalShifterDfuOpcodeAbort, 0x34],
);
expect(
BootloaderDfuProtocol.encodeGetStatusPayload(),
[universalShifterDfuOpcodeGetStatus],
);
});
});
group('DfuProtocol data frame building', () {
test('builds 64-byte frames and handles final partial payload', () {
final image = List<int>.generate(80, (index) => index);
final frames = DfuProtocol.buildDataFrames(image);
group('BootloaderDfuProtocol data frame building', () {
test('builds offset frames with payload CRC and variable final length', () {
final image = List<int>.generate(60, (index) => index);
final frames = BootloaderDfuProtocol.buildDataFrames(
imageBytes: image,
sessionId: 0x7A,
);
expect(frames.length, 2);
expect(frames[0].sequence, 0);
expect(frames[0].sessionId, 0x7A);
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.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, 63);
expect(frames[1].payloadLength, 17);
expect(frames[1].bytes.length, universalShifterDfuFrameSizeBytes);
expect(frames[1].bytes.sublist(1, 18), image.sublist(63, 80));
expect(frames[1].offset, 55);
expect(frames[1].payloadLength, 5);
expect(frames[1].bytes.length, 14);
expect(frames[1].bytes.sublist(1, 5), [55, 0, 0, 0]);
expect(frames[1].bytes.sublist(9), image.sublist(55));
});
test('uses deterministic wrapping sequence numbers from custom start', () {
final image = List<int>.generate(
3 * universalShifterDfuFramePayloadSizeBytes,
(index) => index & 0xFF);
test('uses caller supplied payload size for low-MTU links', () {
final image = List<int>.generate(15, (index) => index);
final frames = BootloaderDfuProtocol.buildDataFrames(
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);
expect(frames[0].sequence, 0xFE);
expect(frames[1].sequence, 0xFF);
expect(frames[2].sequence, 0x00);
test('calculates safe payload size from negotiated MTU', () {
expect(
BootloaderDfuProtocol.maxPayloadSizeForMtu(64),
universalShifterBootloaderDfuMaxPayloadSizeBytes - 3,
);
expect(BootloaderDfuProtocol.maxPayloadSizeForMtu(23), 11);
expect(BootloaderDfuProtocol.maxPayloadSizeForMtu(12), 0);
expect(
BootloaderDfuProtocol.maxPayloadSizeForMtu(128),
universalShifterBootloaderDfuMaxPayloadSizeBytes,
);
});
});
group('DfuProtocol sequence and ACK helpers', () {
test('wraps sequence values and computes ack+1 rewind', () {
expect(DfuProtocol.nextSequence(0x00), 0x01);
expect(DfuProtocol.nextSequence(0xFF), 0x00);
group('BootloaderDfuProtocol status parsing', () {
test('parses bootloader status payload', () {
final status = BootloaderDfuProtocol.parseStatusPayload(
[0x00, 0x22, 0x78, 0x56, 0x34, 0x12],
);
expect(DfuProtocol.rewindSequenceFromAck(0x05), 0x06);
expect(DfuProtocol.rewindSequenceFromAck(0xFF), 0x00);
expect(status.code, DfuBootloaderStatusCode.ok);
expect(status.rawCode, 0x00);
expect(status.sessionId, 0x22);
expect(status.expectedOffset, 0x12345678);
expect(status.isOk, isTrue);
});
test('computes wrapping sequence distance', () {
expect(DfuProtocol.sequenceDistance(250, 2), 8);
expect(DfuProtocol.sequenceDistance(1, 1), 0);
test('preserves unknown status codes', () {
final status = BootloaderDfuProtocol.parseStatusPayload(
[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,
];
}