MSP protocol implementation and settings menu

This commit is contained in:
Constantin Leue
2026-07-30 21:18:48 +02:00
parent 5994aee7ad
commit 078d58dbc0
16 changed files with 1132 additions and 29 deletions
@@ -0,0 +1,91 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:dmc_app/transport/msp/msp_frame.dart';
import 'package:dmc_app/transport/msp/msp_frame_decoder.dart';
/// Fuettert [bytes] in Stuecken der Groesse [chunkSize] und sammelt alle
/// dekodierten Rahmen ueber den gesamten Strom.
List<MspFrame> _decodeInChunks(Uint8List bytes, int chunkSize) {
final decoder = MspFrameDecoder();
final frames = <MspFrame>[];
for (var offset = 0; offset < bytes.length; offset += chunkSize) {
final end = (offset + chunkSize).clamp(0, bytes.length);
frames.addAll(decoder.addBytes(bytes.sublist(offset, end)));
}
return frames;
}
void main() {
final altitudePayload = Uint8List.fromList(
[0xD2, 0x04, 0x00, 0x00, 0xCE, 0xFF, 0xB0, 0x04, 0x00, 0x00],
);
final altitudeFrameBytes =
encodeMspV2Frame(MspDirection.response, 109, altitudePayload);
// Abnahmekriterium (Doku Kommunikationsschicht 8): dieselbe Bytefolge in
// Stuecken von 1, 7 und 512 Byte muss dasselbe Ergebnis liefern.
for (final chunkSize in [1, 7, 512]) {
test('dekodiert einen MSP_ALTITUDE-Rahmen bei Chunk-Groesse $chunkSize', () {
final frames = _decodeInChunks(altitudeFrameBytes, chunkSize);
expect(frames, hasLength(1));
expect(frames.single.direction, MspDirection.response);
expect(frames.single.function, 109);
expect(frames.single.payload, altitudePayload);
});
}
test('mehrere Rahmen hintereinander in einem Chunk werden alle erkannt', () {
final gpsFrameBytes = encodeMspV2Frame(MspDirection.response, 106,
Uint8List.fromList([1, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
final combined = Uint8List.fromList([
...altitudeFrameBytes,
...gpsFrameBytes,
]);
final frames = _decodeInChunks(combined, 512);
expect(frames, hasLength(2));
expect(frames[0].function, 109);
expect(frames[1].function, 106);
});
test('fehlerhafte CRC wird verworfen, ohne den Strom zu verlieren', () {
final corrupted = Uint8List.fromList(altitudeFrameBytes);
corrupted[corrupted.length - 1] ^= 0xFF; // letztes Byte (CRC) kippen
final decoder = MspFrameDecoder();
final framesFromCorrupted = decoder.addBytes(corrupted);
expect(framesFromCorrupted, isEmpty);
// Direkt danach ein gueltiger Rahmen auf demselben Decoder - der
// Automat muss wieder im Idle-Zustand sein und normal weiterlesen.
final framesFromValid = decoder.addBytes(altitudeFrameBytes);
expect(framesFromValid, hasLength(1));
expect(framesFromValid.single.function, 109);
});
test('Fehlerantwort (dir = \'!\') wird als solche erkannt', () {
final errorFrameBytes =
encodeMspV2Frame(MspDirection.error, 109, Uint8List(0));
final frames = MspFrameDecoder().addBytes(errorFrameBytes);
expect(frames, hasLength(1));
expect(frames.single.direction, MspDirection.error);
expect(frames.single.function, 109);
});
test('Muell vor dem eigentlichen Rahmen wird ignoriert', () {
final garbage = Uint8List.fromList([0x00, 0x24, 0x11, 0x58, 0xFF]);
final combined = Uint8List.fromList([...garbage, ...altitudeFrameBytes]);
final frames = _decodeInChunks(combined, 7);
expect(frames, hasLength(1));
expect(frames.single.function, 109);
});
}