feat(dfu): add packet codec and crc32 utilities
This commit is contained in:
137
lib/service/dfu_protocol.dart
Normal file
137
lib/service/dfu_protocol.dart
Normal file
@ -0,0 +1,137 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||
|
||||
const int _startPayloadLength = 11;
|
||||
|
||||
class DfuStartPayload {
|
||||
const DfuStartPayload({
|
||||
required this.totalLength,
|
||||
required this.imageCrc32,
|
||||
required this.sessionId,
|
||||
required this.flags,
|
||||
});
|
||||
|
||||
final int totalLength;
|
||||
final int imageCrc32;
|
||||
final int sessionId;
|
||||
final int flags;
|
||||
}
|
||||
|
||||
class DfuDataFrame {
|
||||
const DfuDataFrame({
|
||||
required this.sequence,
|
||||
required this.offset,
|
||||
required this.payloadLength,
|
||||
required this.bytes,
|
||||
});
|
||||
|
||||
final int sequence;
|
||||
final int offset;
|
||||
final int payloadLength;
|
||||
final Uint8List bytes;
|
||||
}
|
||||
|
||||
class DfuProtocol {
|
||||
const DfuProtocol._();
|
||||
|
||||
static Uint8List encodeStartPayload(DfuStartPayload payload) {
|
||||
final data = ByteData(_startPayloadLength);
|
||||
data.setUint8(0, universalShifterDfuOpcodeStart);
|
||||
data.setUint32(1, payload.totalLength, Endian.little);
|
||||
data.setUint32(5, payload.imageCrc32, Endian.little);
|
||||
data.setUint8(9, payload.sessionId);
|
||||
data.setUint8(10, payload.flags);
|
||||
return data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
static Uint8List encodeFinishPayload() {
|
||||
return Uint8List.fromList([universalShifterDfuOpcodeFinish]);
|
||||
}
|
||||
|
||||
static Uint8List encodeAbortPayload() {
|
||||
return Uint8List.fromList([universalShifterDfuOpcodeAbort]);
|
||||
}
|
||||
|
||||
static List<DfuDataFrame> buildDataFrames(
|
||||
List<int> imageBytes, {
|
||||
int startSequence = 0,
|
||||
}) {
|
||||
final frames = <DfuDataFrame>[];
|
||||
var seq = _asU8(startSequence);
|
||||
var offset = 0;
|
||||
while (offset < imageBytes.length) {
|
||||
final remaining = imageBytes.length - offset;
|
||||
final chunkLength = remaining < universalShifterDfuFramePayloadSizeBytes
|
||||
? remaining
|
||||
: universalShifterDfuFramePayloadSizeBytes;
|
||||
|
||||
final frame = Uint8List(universalShifterDfuFrameSizeBytes);
|
||||
frame[0] = seq;
|
||||
frame.setRange(1, 1 + chunkLength, imageBytes, offset);
|
||||
|
||||
frames.add(
|
||||
DfuDataFrame(
|
||||
sequence: seq,
|
||||
offset: offset,
|
||||
payloadLength: chunkLength,
|
||||
bytes: frame,
|
||||
),
|
||||
);
|
||||
|
||||
offset += chunkLength;
|
||||
seq = nextSequence(seq);
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
static int nextSequence(int sequence) {
|
||||
return _asU8(sequence + 1);
|
||||
}
|
||||
|
||||
static int rewindSequenceFromAck(int acknowledgedSequence) {
|
||||
return nextSequence(acknowledgedSequence);
|
||||
}
|
||||
|
||||
static int sequenceDistance(int from, int to) {
|
||||
return _asU8(to - from);
|
||||
}
|
||||
|
||||
static int parseAckPayload(List<int> payload) {
|
||||
if (payload.length != 1) {
|
||||
throw const FormatException('ACK payload must be exactly 1 byte.');
|
||||
}
|
||||
return _asU8(payload.first);
|
||||
}
|
||||
|
||||
static const int crc32Initial = 0xFFFFFFFF;
|
||||
static const int _crc32PolynomialReflected = 0xEDB88320;
|
||||
|
||||
static int crc32Update(int crc, List<int> bytes) {
|
||||
var next = crc & 0xFFFFFFFF;
|
||||
for (final byte in bytes) {
|
||||
next ^= byte;
|
||||
for (var bit = 0; bit < 8; bit++) {
|
||||
if ((next & 0x1) != 0) {
|
||||
next = (next >> 1) ^ _crc32PolynomialReflected;
|
||||
} else {
|
||||
next >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return next & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
static int crc32Finalize(int crc) {
|
||||
return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
static int crc32(List<int> bytes) {
|
||||
return crc32Finalize(crc32Update(crc32Initial, bytes));
|
||||
}
|
||||
|
||||
static int _asU8(int value) {
|
||||
return value & 0xFF;
|
||||
}
|
||||
}
|
||||
86
test/service/dfu_protocol_test.dart
Normal file
86
test/service/dfu_protocol_test.dart
Normal file
@ -0,0 +1,86 @@
|
||||
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||
import 'package:abawo_bt_app/service/dfu_protocol.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('DfuProtocol CRC32', () {
|
||||
test('matches known vector', () {
|
||||
final crc = DfuProtocol.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(
|
||||
totalLength: 0x1234,
|
||||
imageCrc32: 0x89ABCDEF,
|
||||
sessionId: 0x22,
|
||||
flags: universalShifterDfuFlagEncrypted,
|
||||
),
|
||||
);
|
||||
|
||||
expect(payload.length, 11);
|
||||
expect(
|
||||
payload,
|
||||
[
|
||||
universalShifterDfuOpcodeStart,
|
||||
0x34,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0xEF,
|
||||
0xCD,
|
||||
0xAB,
|
||||
0x89,
|
||||
0x22,
|
||||
universalShifterDfuFlagEncrypted,
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('encodes FINISH and ABORT payloads as one byte', () {
|
||||
expect(
|
||||
DfuProtocol.encodeFinishPayload(), [universalShifterDfuOpcodeFinish]);
|
||||
expect(
|
||||
DfuProtocol.encodeAbortPayload(), [universalShifterDfuOpcodeAbort]);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(frames.length, 2);
|
||||
|
||||
expect(frames[0].sequence, 0);
|
||||
expect(frames[0].offset, 0);
|
||||
expect(frames[0].payloadLength, universalShifterDfuFramePayloadSizeBytes);
|
||||
expect(frames[0].bytes.length, universalShifterDfuFrameSizeBytes);
|
||||
expect(frames[0].bytes.sublist(1, 64), image.sublist(0, 63));
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(DfuProtocol.rewindSequenceFromAck(0x05), 0x06);
|
||||
expect(DfuProtocol.rewindSequenceFromAck(0xFF), 0x00);
|
||||
});
|
||||
|
||||
test('computes wrapping sequence distance', () {
|
||||
expect(DfuProtocol.sequenceDistance(250, 2), 8);
|
||||
expect(DfuProtocol.sequenceDistance(1, 1), 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user