Initial version
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:openscooter/scooters/apollo_protocol.dart';
|
||||
import 'package:openscooter/scooters/scooter.dart';
|
||||
|
||||
Uint8List hex(String s) => Uint8List.fromList(
|
||||
s.trim().split(RegExp(r'\s+')).map((h) => int.parse(h, radix: 16)).toList(),
|
||||
);
|
||||
|
||||
/// Synthetic monitor frame from the brief (section 35). CRC valid.
|
||||
final monitorFrame =
|
||||
hex('AA 00 00 00 02 4B 00 FA 00 C8 01 E0 01 40 23 28 00 7B 00 30 39 8A 24 E9 1F');
|
||||
|
||||
/// Synthetic base frame from the brief (section 46). CRC valid.
|
||||
final baseFrame =
|
||||
hex('AB 01 00 19 06 0C 14 1E 80 00 1F 00 27 10 13 88 00 64 12 AB 02 05 11 F0 C0');
|
||||
|
||||
/// Re-signs a frame after editing its payload so parser tests only fail on
|
||||
/// the field under test, never on CRC.
|
||||
Uint8List resign(Uint8List frame) {
|
||||
final out = Uint8List.fromList(frame);
|
||||
final crc = apolloCrc16(out.sublist(0, out.length - 2));
|
||||
out[out.length - 2] = crc & 0xFF;
|
||||
out[out.length - 1] = crc >> 8;
|
||||
return out;
|
||||
}
|
||||
|
||||
Uint8List patched(Uint8List frame, Map<int, int> bytes) {
|
||||
final out = Uint8List.fromList(frame);
|
||||
bytes.forEach((i, v) => out[i] = v);
|
||||
return resign(out);
|
||||
}
|
||||
|
||||
/// LIVE captures from an Apollo Go, 2026-09-21, idle, full battery,
|
||||
/// headlight off. Regression fixtures per brief section 81.
|
||||
final realCmd0Idle =
|
||||
hex('AA 00 19 01 02 64 00 00 00 00 01 9F 00 00 16 19 00 00 00 0C C5 0E 22 B3 13');
|
||||
final realCmd0HeadlightCruise =
|
||||
hex('AA 00 19 01 02 64 00 00 00 00 01 9D 00 00 16 19 00 00 00 0C C5 8E 26 2A D7');
|
||||
final realCmd0RightSignal =
|
||||
hex('AA 00 19 01 02 64 00 00 00 00 01 9E 00 00 16 19 00 00 00 0C C5 2E 2E 57 15');
|
||||
final realCmd0LeftSignal =
|
||||
hex('AA 00 19 01 02 64 00 00 00 00 01 9E 00 00 16 19 00 00 00 0C C5 4E 36 7F 1F');
|
||||
final realCmd1Base =
|
||||
hex('AA 01 19 03 0A 0F 1E D8 80 00 1F 00 00 00 00 00 00 00 00 00 00 00 00 68 61');
|
||||
|
||||
void main() {
|
||||
group('real Apollo Go captures', () {
|
||||
test('all fixtures are CRC valid', () {
|
||||
for (final f in [realCmd0Idle, realCmd0HeadlightCruise, realCmd0RightSignal,
|
||||
realCmd0LeftSignal, realCmd1Base]) {
|
||||
expect(apolloValidateFrame(f), isTrue, reason: apolloHex(f));
|
||||
}
|
||||
});
|
||||
|
||||
test('real cmd1 base frame', () {
|
||||
final b = parseApolloBaseFrame(realCmd1Base);
|
||||
expect(b.limitCruise, 3);
|
||||
expect(b.limitMode1, 10);
|
||||
expect(b.limitMode2, 15);
|
||||
expect(b.limitMode3, 30);
|
||||
expect(b.batteryTemperature, -40);
|
||||
expect(b.totalBatteryCapacity, 0);
|
||||
expect(b.remainingBatteryCapacity, 0);
|
||||
expect(b.batteryCycles, 0);
|
||||
expect(b.displayId, isNull);
|
||||
expect(b.displayVersion, 'V0.0.0');
|
||||
expect(b.faultEnable, isTrue);
|
||||
expect([b.ctrlSn, b.ctrlMp3, b.ctrlRgb, b.ctrlBms, b.internalSpeedScalingFlag],
|
||||
everyElement(isTrue));
|
||||
});
|
||||
|
||||
test('real cmd0 idle frame with the Go scaling flag', () {
|
||||
final m = parseApolloMonitorFrame(realCmd0Idle, internalSpeedScalingFlag: true);
|
||||
expect(m.gear, 2);
|
||||
expect(m.batteryLevel, 100);
|
||||
expect(m.rawSpeed, 0);
|
||||
expect(m.speed, 0.0);
|
||||
expect(m.voltage, 41.5);
|
||||
expect(m.current, 0.0);
|
||||
expect(m.power, 0.0);
|
||||
expect(m.motorTemperature, 22);
|
||||
expect(m.controllerTemperature, 25);
|
||||
expect(m.tripDistance, 0.0);
|
||||
expect(m.odometer, 326.9);
|
||||
expect(m.atmosphereLight, isTrue);
|
||||
expect(m.unlocked, isTrue);
|
||||
expect(m.headlight, isFalse);
|
||||
expect(m.leftTurnSignal, isFalse);
|
||||
expect(m.rightTurnSignal, isFalse);
|
||||
expect(m.cruiseControl, isFalse);
|
||||
expect(m.imperial, isTrue);
|
||||
expect(m.bootMode, isFalse);
|
||||
});
|
||||
|
||||
test('real cmd0 variants: headlight+cruise, right signal, left signal', () {
|
||||
final h = parseApolloMonitorFrame(realCmd0HeadlightCruise, internalSpeedScalingFlag: true);
|
||||
expect(h.headlight, isTrue);
|
||||
expect(h.cruiseControl, isTrue);
|
||||
expect(h.voltage, 41.3);
|
||||
final r = parseApolloMonitorFrame(realCmd0RightSignal, internalSpeedScalingFlag: true);
|
||||
expect(r.rightTurnSignal, isTrue);
|
||||
expect(r.leftTurnSignal, isFalse);
|
||||
final l = parseApolloMonitorFrame(realCmd0LeftSignal, internalSpeedScalingFlag: true);
|
||||
expect(l.leftTurnSignal, isTrue);
|
||||
expect(l.rightTurnSignal, isFalse);
|
||||
});
|
||||
|
||||
test('real 20 + 5 byte notification split reassembles', () {
|
||||
final buf = ApolloFrameBuffer();
|
||||
expect(buf.add(realCmd0Idle.sublist(0, 20)), isEmpty);
|
||||
expect(buf.add(realCmd0Idle.sublist(20)), [realCmd0Idle]);
|
||||
expect(buf.add(realCmd1Base.sublist(0, 20)), isEmpty);
|
||||
expect(buf.add(realCmd1Base.sublist(20)), [realCmd1Base]);
|
||||
});
|
||||
});
|
||||
|
||||
group('CRC-16', () {
|
||||
test('vector 1 from brief', () {
|
||||
expect(apolloCrc16(hex('AB 00 0A 00 00 00 00 00')), 0x6A0B);
|
||||
});
|
||||
test('vector 2 from brief', () {
|
||||
expect(apolloCrc16(hex('AB 00 0A 80 19 06 0C 14')), 0xE6E8);
|
||||
});
|
||||
test('wire order is little-endian and covers header through length-3', () {
|
||||
final f = hex('AB 00 0A 00 00 00 00 00 0B 6A');
|
||||
expect(apolloValidateFrame(f), isTrue);
|
||||
expect(apolloValidateFrame(hex('AB 00 0A 00 00 00 00 00 6A 0B')), isFalse);
|
||||
});
|
||||
test('synthetic frames validate', () {
|
||||
expect(apolloValidateFrame(monitorFrame), isTrue);
|
||||
expect(apolloValidateFrame(baseFrame), isTrue);
|
||||
});
|
||||
test('corrupting one byte fails validation', () {
|
||||
final bad = Uint8List.fromList(monitorFrame)..[5] ^= 0x01;
|
||||
expect(apolloValidateFrame(bad), isFalse);
|
||||
// Including unknown metadata bytes 2 and 3: they are CRC-covered.
|
||||
final bad2 = Uint8List.fromList(monitorFrame)..[2] = 0x01;
|
||||
expect(apolloValidateFrame(bad2), isFalse);
|
||||
});
|
||||
test('keepalive constant is not frame-CRC signed', () {
|
||||
expect(apolloKeepalivePacket, hex('A5 02 FD 5A'));
|
||||
expect(apolloValidateFrame(apolloKeepalivePacket), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('PIN command', () {
|
||||
test('builds AT+PWD[pin] with no line ending', () {
|
||||
expect(String.fromCharCodes(buildApolloPinCommand('123456')), 'AT+PWD[123456]');
|
||||
expect(buildApolloPinCommand('123456').last, ']'.codeUnitAt(0));
|
||||
});
|
||||
test('rejects non six-digit input', () {
|
||||
expect(() => buildApolloPinCommand('12345'), throwsArgumentError);
|
||||
expect(() => buildApolloPinCommand('12345a'), throwsArgumentError);
|
||||
expect(() => buildApolloPinCommand('1234567'), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
group('PIN response parser', () {
|
||||
Uint8List ascii(String s) => Uint8List.fromList(s.codeUnits);
|
||||
|
||||
test('OK+PWD:Y is success', () {
|
||||
expect(parseApolloPinResponse(ascii('OK+PWD:Y')), AuthenticationResult.success);
|
||||
});
|
||||
test('OK+PWD:N is invalid credential', () {
|
||||
expect(parseApolloPinResponse(ascii('OK+PWD:N')), AuthenticationResult.invalidCredential);
|
||||
});
|
||||
test('search within garbage', () {
|
||||
expect(parseApolloPinResponse(ascii('garbageOK+PWD:Y\r\n')), AuthenticationResult.success);
|
||||
});
|
||||
test('NUL bytes are ignored', () {
|
||||
expect(parseApolloPinResponse(ascii('OK+PWD\x00:Y')), AuthenticationResult.success);
|
||||
});
|
||||
test('partial is null', () {
|
||||
expect(parseApolloPinResponse(ascii('OK+PW')), isNull);
|
||||
expect(parseApolloPinResponse(ascii('')), isNull);
|
||||
});
|
||||
test('fragmented response reassembles through the AT buffer', () {
|
||||
final buf = ApolloAtBuffer();
|
||||
buf.add(ascii('OK+PW'));
|
||||
expect(parseApolloPinResponse(buf.bytes), isNull);
|
||||
buf.add(ascii('D:Y'));
|
||||
expect(parseApolloPinResponse(buf.bytes), AuthenticationResult.success);
|
||||
});
|
||||
test('AT buffer is capped', () {
|
||||
final buf = ApolloAtBuffer(maxLength: 16);
|
||||
buf.add(Uint8List.fromList(List.filled(100, 0x41)));
|
||||
expect(buf.bytes.length, 16);
|
||||
});
|
||||
});
|
||||
|
||||
group('frame lengths', () {
|
||||
test('A5 02 is 4, other A5 is 8, AA/AB cmd 0/1 is 25', () {
|
||||
expect(apolloFrameLength(0xA5, 0x02), 4);
|
||||
expect(apolloFrameLength(0xA5, 0x01), 8);
|
||||
expect(apolloFrameLength(0xAA, 0x00), 25);
|
||||
expect(apolloFrameLength(0xAA, 0x01), 25);
|
||||
expect(apolloFrameLength(0xAB, 0x00), 25);
|
||||
expect(apolloFrameLength(0xAB, 0x01), 25);
|
||||
expect(apolloFrameLength(0xAB, 0x02), isNull);
|
||||
expect(apolloFrameLength(0x00, 0x00), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApolloFrameBuffer', () {
|
||||
late ApolloFrameBuffer buf;
|
||||
setUp(() => buf = ApolloFrameBuffer());
|
||||
|
||||
test('one complete frame', () {
|
||||
expect(buf.add(monitorFrame), [monitorFrame]);
|
||||
expect(buf.length, 0);
|
||||
});
|
||||
|
||||
test('frame split into two chunks', () {
|
||||
expect(buf.add(monitorFrame.sublist(0, 10)), isEmpty);
|
||||
expect(buf.add(monitorFrame.sublist(10)), [monitorFrame]);
|
||||
});
|
||||
|
||||
test('frame split byte-by-byte', () {
|
||||
final out = <Uint8List>[];
|
||||
for (final b in monitorFrame) {
|
||||
out.addAll(buf.add(Uint8List.fromList([b])));
|
||||
}
|
||||
expect(out, [monitorFrame]);
|
||||
});
|
||||
|
||||
test('two frames in one notification', () {
|
||||
final both = Uint8List.fromList([...monitorFrame, ...baseFrame]);
|
||||
expect(buf.add(both), [monitorFrame, baseFrame]);
|
||||
});
|
||||
|
||||
test('one complete plus half of next', () {
|
||||
final data = Uint8List.fromList([...monitorFrame, ...baseFrame.sublist(0, 12)]);
|
||||
expect(buf.add(data), [monitorFrame]);
|
||||
expect(buf.add(baseFrame.sublist(12)), [baseFrame]);
|
||||
});
|
||||
|
||||
test('garbage before a valid frame', () {
|
||||
final data = Uint8List.fromList([0x00, 0x11, 0xFF, 0xAA, ...monitorFrame]);
|
||||
expect(buf.add(data), [monitorFrame]);
|
||||
});
|
||||
|
||||
test('garbage between valid frames', () {
|
||||
final data = Uint8List.fromList([...monitorFrame, 0x13, 0xAB, 0x00, ...baseFrame]);
|
||||
expect(buf.add(data), [monitorFrame, baseFrame]);
|
||||
});
|
||||
|
||||
test('bad CRC frame followed by a valid frame', () {
|
||||
final bad = Uint8List.fromList(monitorFrame)..[24] ^= 0xFF;
|
||||
final data = Uint8List.fromList([...bad, ...baseFrame]);
|
||||
expect(buf.add(data), [baseFrame]);
|
||||
});
|
||||
|
||||
test('unsupported head byte and unsupported command are skipped', () {
|
||||
final data = Uint8List.fromList([0xCC, 0xAB, 0x07, ...monitorFrame]);
|
||||
expect(buf.add(data), [monitorFrame]);
|
||||
});
|
||||
|
||||
test('exact keepalive is emitted, other A5 sequences are skipped', () {
|
||||
expect(buf.add(apolloKeepalivePacket), [apolloKeepalivePacket]);
|
||||
final data = Uint8List.fromList([0xA5, 0x02, 0x00, 0x00, ...monitorFrame]);
|
||||
expect(buf.add(data), [monitorFrame]);
|
||||
});
|
||||
|
||||
test('buffer size cap keeps the newest bytes', () {
|
||||
final small = ApolloFrameBuffer(maxLength: 64);
|
||||
small.add(Uint8List.fromList(List.filled(1000, 0x01)));
|
||||
expect(small.length, lessThanOrEqualTo(64));
|
||||
// Still recovers afterwards.
|
||||
expect(small.add(monitorFrame), [monitorFrame]);
|
||||
});
|
||||
|
||||
test('a garbage byte does not wedge parsing permanently', () {
|
||||
buf.add(Uint8List.fromList([0xAA]));
|
||||
buf.add(Uint8List.fromList([0x00]));
|
||||
// 23 more junk bytes complete a fake 25-byte candidate with a bad CRC.
|
||||
buf.add(Uint8List.fromList(List.filled(23, 0x55)));
|
||||
expect(buf.add(monitorFrame), [monitorFrame]);
|
||||
});
|
||||
});
|
||||
|
||||
group('monitor parser', () {
|
||||
test('synthetic vector, scaling flag true', () {
|
||||
final m = parseApolloMonitorFrame(monitorFrame, internalSpeedScalingFlag: true);
|
||||
expect(m.gear, 2);
|
||||
expect(m.batteryLevel, 75);
|
||||
expect(m.rawSpeed, 250);
|
||||
expect(m.speed, 25.0);
|
||||
expect(m.voltage, 48.0);
|
||||
expect(m.current, 5.0);
|
||||
expect(m.power, 240.0);
|
||||
expect(m.motorTemperature, 35);
|
||||
expect(m.controllerTemperature, 40);
|
||||
expect(m.tripDistance, 12.3);
|
||||
expect(m.odometer, 1234.5);
|
||||
expect(m.atmosphereLight, isTrue);
|
||||
expect(m.unlocked, isTrue);
|
||||
expect(m.headlight, isTrue);
|
||||
expect(m.rightTurnSignal, isFalse);
|
||||
expect(m.leftTurnSignal, isFalse);
|
||||
expect(m.cruiseControl, isTrue);
|
||||
expect(m.imperial, isTrue);
|
||||
expect(m.bootMode, isFalse);
|
||||
});
|
||||
|
||||
test('scaling flag false gives raw/1000', () {
|
||||
final m = parseApolloMonitorFrame(monitorFrame, internalSpeedScalingFlag: false);
|
||||
expect(m.speed, 0.25);
|
||||
});
|
||||
|
||||
test('speed uses the max of source A and B', () {
|
||||
// A = 0x00FA (250), B = 0x00C8 (200) in the vector. Swap them.
|
||||
final swapped = patched(monitorFrame, {6: 0x00, 7: 0xC8, 8: 0x00, 9: 0xFA});
|
||||
expect(parseApolloMonitorFrame(swapped, internalSpeedScalingFlag: true).rawSpeed, 250);
|
||||
final bBigger = patched(monitorFrame, {8: 0x01, 9: 0x00});
|
||||
expect(parseApolloMonitorFrame(bBigger, internalSpeedScalingFlag: true).rawSpeed, 256);
|
||||
});
|
||||
|
||||
test('negative current and derived power', () {
|
||||
// -5.0 A = -320 = 0xFEC0
|
||||
final f = patched(monitorFrame, {12: 0xFE, 13: 0xC0});
|
||||
final m = parseApolloMonitorFrame(f, internalSpeedScalingFlag: true);
|
||||
expect(m.current, -5.0);
|
||||
expect(m.power, -240.0);
|
||||
});
|
||||
|
||||
test('power is rounded to one decimal', () {
|
||||
// voltage 48.1 (0x01E1), current 1/64 A = 0.015625 -> 0.7515625 -> 0.8
|
||||
final f = patched(monitorFrame, {10: 0x01, 11: 0xE1, 12: 0x00, 13: 0x01});
|
||||
expect(parseApolloMonitorFrame(f, internalSpeedScalingFlag: true).power, 0.8);
|
||||
});
|
||||
|
||||
test('negative temperatures', () {
|
||||
final f = patched(monitorFrame, {14: 0xF6, 15: 0xEC}); // -10, -20
|
||||
final m = parseApolloMonitorFrame(f, internalSpeedScalingFlag: true);
|
||||
expect(m.motorTemperature, -10);
|
||||
expect(m.controllerTemperature, -20);
|
||||
});
|
||||
|
||||
test('24-bit odometer uses all three bytes', () {
|
||||
final f = patched(monitorFrame, {18: 0x12, 19: 0x34, 20: 0x56});
|
||||
expect(parseApolloMonitorFrame(f, internalSpeedScalingFlag: true).odometer, 0x123456 / 10.0);
|
||||
});
|
||||
|
||||
test('each switch flag is independent', () {
|
||||
final base = patched(monitorFrame, {21: 0x00, 22: 0x00});
|
||||
var m = parseApolloMonitorFrame(base, internalSpeedScalingFlag: true);
|
||||
expect([m.atmosphereLight, m.unlocked, m.rightTurnSignal, m.leftTurnSignal, m.headlight,
|
||||
m.cruiseControl, m.imperial, m.bootMode],
|
||||
everyElement(isFalse));
|
||||
|
||||
m = parseApolloMonitorFrame(patched(base, {21: 1 << 1}), internalSpeedScalingFlag: true);
|
||||
expect(m.atmosphereLight, isTrue);
|
||||
m = parseApolloMonitorFrame(patched(base, {21: 1 << 3}), internalSpeedScalingFlag: true);
|
||||
expect(m.unlocked, isTrue);
|
||||
m = parseApolloMonitorFrame(patched(base, {21: 1 << 5}), internalSpeedScalingFlag: true);
|
||||
expect(m.rightTurnSignal, isTrue);
|
||||
m = parseApolloMonitorFrame(patched(base, {21: 1 << 6}), internalSpeedScalingFlag: true);
|
||||
expect(m.leftTurnSignal, isTrue);
|
||||
m = parseApolloMonitorFrame(patched(base, {21: 1 << 7}), internalSpeedScalingFlag: true);
|
||||
expect(m.headlight, isTrue);
|
||||
m = parseApolloMonitorFrame(patched(base, {22: 1 << 2}), internalSpeedScalingFlag: true);
|
||||
expect(m.cruiseControl, isTrue);
|
||||
m = parseApolloMonitorFrame(patched(base, {22: 1 << 5}), internalSpeedScalingFlag: true);
|
||||
expect(m.imperial, isTrue);
|
||||
m = parseApolloMonitorFrame(patched(base, {22: 1 << 6}), internalSpeedScalingFlag: true);
|
||||
expect(m.bootMode, isTrue);
|
||||
});
|
||||
|
||||
test('corrupt byte is rejected by CRC', () {
|
||||
final bad = Uint8List.fromList(monitorFrame)..[4] = 0x03;
|
||||
expect(() => parseApolloMonitorFrame(bad, internalSpeedScalingFlag: true),
|
||||
throwsFormatException);
|
||||
});
|
||||
|
||||
test('rejects wrong command or length', () {
|
||||
expect(() => parseApolloMonitorFrame(baseFrame, internalSpeedScalingFlag: true),
|
||||
throwsFormatException);
|
||||
expect(() => parseApolloMonitorFrame(monitorFrame.sublist(1), internalSpeedScalingFlag: true),
|
||||
throwsFormatException);
|
||||
});
|
||||
});
|
||||
|
||||
group('base parser', () {
|
||||
test('synthetic vector', () {
|
||||
final b = parseApolloBaseFrame(baseFrame);
|
||||
expect(b.limitCruise, 25);
|
||||
expect(b.limitMode1, 6);
|
||||
expect(b.limitMode2, 12);
|
||||
expect(b.limitMode3, 20);
|
||||
expect(b.batteryTemperature, 30);
|
||||
expect(b.totalBatteryCapacity, 10000);
|
||||
expect(b.remainingBatteryCapacity, 5000);
|
||||
expect(b.batteryCycles, 100);
|
||||
expect(b.displayId, '12ab');
|
||||
expect(b.displayVersion, 'V2.5.17');
|
||||
expect(b.ctrlSn, isTrue);
|
||||
expect(b.ctrlMp3, isTrue);
|
||||
expect(b.ctrlRgb, isTrue);
|
||||
expect(b.ctrlBms, isTrue);
|
||||
expect(b.internalSpeedScalingFlag, isTrue);
|
||||
expect(b.faultEnable, isTrue); // frame[8] = 0x80
|
||||
expect(b.e9, isFalse);
|
||||
});
|
||||
|
||||
test('negative battery temperature', () {
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {7: 0xF1})).batteryTemperature, -15);
|
||||
});
|
||||
|
||||
test('capacity fields are big-endian', () {
|
||||
final b = parseApolloBaseFrame(patched(baseFrame, {12: 0x01, 13: 0x02, 14: 0x03, 15: 0x04, 16: 0x05, 17: 0x06}));
|
||||
expect(b.totalBatteryCapacity, 0x0102);
|
||||
expect(b.remainingBatteryCapacity, 0x0304);
|
||||
expect(b.batteryCycles, 0x0506);
|
||||
});
|
||||
|
||||
test('display id 0000 is absent, otherwise lowercase hex padded', () {
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {18: 0, 19: 0})).displayId, isNull);
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {18: 0x0A, 19: 0x0B})).displayId, '0a0b');
|
||||
});
|
||||
|
||||
test('display version formats V%d.%d.%d', () {
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {20: 0, 21: 10, 22: 255})).displayVersion, 'V0.10.255');
|
||||
});
|
||||
|
||||
test('capability bits are independent', () {
|
||||
final none = parseApolloBaseFrame(patched(baseFrame, {10: 0x00}));
|
||||
expect([none.ctrlSn, none.ctrlMp3, none.ctrlRgb, none.ctrlBms, none.internalSpeedScalingFlag],
|
||||
everyElement(isFalse));
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {10: 1 << 0})).ctrlSn, isTrue);
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {10: 1 << 1})).ctrlMp3, isTrue);
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {10: 1 << 2})).ctrlRgb, isTrue);
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {10: 1 << 3})).ctrlBms, isTrue);
|
||||
expect(parseApolloBaseFrame(patched(baseFrame, {10: 1 << 4})).internalSpeedScalingFlag, isTrue);
|
||||
});
|
||||
|
||||
test('fault flags, including the duplicated frame[8] bit 3 mapping', () {
|
||||
final a = parseApolloBaseFrame(patched(baseFrame, {8: (1 << 1) | (1 << 2), 9: 0}));
|
||||
expect(a.e9, isTrue);
|
||||
expect(a.f1, isTrue);
|
||||
expect(a.f2, isFalse);
|
||||
expect(a.ctrlFaultEarlyWarning, isFalse);
|
||||
expect(a.faultEnable, isFalse);
|
||||
|
||||
// Apollo 4.8.18340 maps both f2 and ctrlFaultEarlyWarning to bit 3.
|
||||
final b = parseApolloBaseFrame(patched(baseFrame, {8: 1 << 3}));
|
||||
expect(b.f2, isTrue);
|
||||
expect(b.ctrlFaultEarlyWarning, isTrue);
|
||||
|
||||
final c = parseApolloBaseFrame(patched(baseFrame, {9: (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 7)}));
|
||||
expect([c.e1, c.e2, c.e3, c.e4, c.e7], everyElement(isTrue));
|
||||
final d = parseApolloBaseFrame(patched(baseFrame, {9: 0}));
|
||||
expect([d.e1, d.e2, d.e3, d.e4, d.e7], everyElement(isFalse));
|
||||
});
|
||||
|
||||
test('corrupt byte is rejected by CRC', () {
|
||||
final bad = Uint8List.fromList(baseFrame)..[3] = 0x20;
|
||||
expect(() => parseApolloBaseFrame(bad), throwsFormatException);
|
||||
});
|
||||
});
|
||||
|
||||
group('set-base packet builder', () {
|
||||
Uint8List build({
|
||||
int gear = 2,
|
||||
bool headlight = true,
|
||||
bool atmosphereLight = false,
|
||||
bool cruiseControl = false,
|
||||
bool bootMode = false,
|
||||
bool imperial = true,
|
||||
bool unlocked = false,
|
||||
}) =>
|
||||
buildApolloSetBasePacket(
|
||||
gearPosition: gear,
|
||||
headlight: headlight,
|
||||
atmosphereLight: atmosphereLight,
|
||||
cruiseControl: cruiseControl,
|
||||
bootMode: bootMode,
|
||||
imperial: imperial,
|
||||
unlocked: unlocked,
|
||||
limitCruise: 25,
|
||||
limitMode1: 6,
|
||||
limitMode2: 12,
|
||||
limitMode3: 20,
|
||||
);
|
||||
|
||||
test('exact packet for brief vector 1 (all zero)', () {
|
||||
final p = buildApolloSetBasePacket(
|
||||
gearPosition: 0,
|
||||
headlight: false,
|
||||
atmosphereLight: false,
|
||||
cruiseControl: false,
|
||||
bootMode: false,
|
||||
imperial: false,
|
||||
unlocked: false,
|
||||
limitCruise: 0,
|
||||
limitMode1: 0,
|
||||
limitMode2: 0,
|
||||
limitMode3: 0,
|
||||
);
|
||||
expect(p, hex('AB 00 0A 00 00 00 00 00 0B 6A'));
|
||||
});
|
||||
|
||||
test('exact packet for brief vector 2 (unlocked with limits)', () {
|
||||
final p = buildApolloSetBasePacket(
|
||||
gearPosition: 0,
|
||||
headlight: false,
|
||||
atmosphereLight: false,
|
||||
cruiseControl: false,
|
||||
bootMode: false,
|
||||
imperial: false,
|
||||
unlocked: true,
|
||||
limitCruise: 25,
|
||||
limitMode1: 6,
|
||||
limitMode2: 12,
|
||||
limitMode3: 20,
|
||||
);
|
||||
expect(p, hex('AB 00 0A 80 19 06 0C 14 E8 E6'));
|
||||
expect(apolloValidateFrame(p), isTrue);
|
||||
});
|
||||
|
||||
test('is exactly 10 bytes with header AB 00 0A and the four limits', () {
|
||||
final p = build();
|
||||
expect(p.length, 10);
|
||||
expect(p.sublist(0, 3), hex('AB 00 0A'));
|
||||
expect(p.sublist(4, 8), [25, 6, 12, 20]);
|
||||
});
|
||||
|
||||
/// Asserts that toggling one control changes exactly one FLAGS bit and
|
||||
/// nothing else before the CRC.
|
||||
void expectOnlyBitChanges(Uint8List a, Uint8List b, int bit) {
|
||||
expect(a.sublist(0, 3), b.sublist(0, 3));
|
||||
expect(a.sublist(4, 8), b.sublist(4, 8), reason: 'speed limits must be preserved');
|
||||
expect(a[3] ^ b[3], 1 << bit, reason: 'only FLAGS bit $bit may change');
|
||||
expect(apolloValidateFrame(a), isTrue);
|
||||
expect(apolloValidateFrame(b), isTrue);
|
||||
}
|
||||
|
||||
test('unlock changes only FLAGS bit 7', () {
|
||||
expectOnlyBitChanges(build(unlocked: false), build(unlocked: true), 7);
|
||||
});
|
||||
test('headlight changes only FLAGS bit 2', () {
|
||||
expectOnlyBitChanges(build(headlight: false), build(headlight: true), 2);
|
||||
});
|
||||
test('atmosphere light changes only FLAGS bit 3', () {
|
||||
expectOnlyBitChanges(build(atmosphereLight: false), build(atmosphereLight: true), 3);
|
||||
});
|
||||
test('cruise changes only FLAGS bit 4', () {
|
||||
expectOnlyBitChanges(build(cruiseControl: false), build(cruiseControl: true), 4);
|
||||
});
|
||||
test('boot mode bit 5 and imperial bit 6', () {
|
||||
expectOnlyBitChanges(build(bootMode: false), build(bootMode: true), 5);
|
||||
expectOnlyBitChanges(build(imperial: false), build(imperial: true), 6);
|
||||
});
|
||||
test('gear occupies bits 0-1 only', () {
|
||||
expect(build(gear: 0)[3] & 0x03, 0);
|
||||
expect(build(gear: 3)[3] & 0x03, 3);
|
||||
expect(build(gear: 7)[3] & 0x03, 3);
|
||||
});
|
||||
test('rejects limits outside one byte', () {
|
||||
expect(
|
||||
() => buildApolloSetBasePacket(
|
||||
gearPosition: 0, headlight: false, atmosphereLight: false, cruiseControl: false,
|
||||
bootMode: false, imperial: false, unlocked: false,
|
||||
limitCruise: 256, limitMode1: 0, limitMode2: 0, limitMode3: 0,
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user