feat: add bootloader DFU protocol validation
This commit is contained in:
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user