import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:hvbt_dash/core/protocol/kpro_parser.dart'; import 'package:hvbt_dash/core/protocol/neg8.dart'; import 'package:hvbt_dash/core/protocol/temp_table.dart'; /// Builds a zeroed 128-byte KPro frame and stamps the NEG8 checksum at byte 127. Uint8List _buildKProFrame({ int rpm = 0, int vss = 0, int tps = 0, int map1 = 0, int map2 = 0, int ign = 0, int ect = 0x80, // tempXlt[0x80]=73 → 33°C int bat = 0, int sw1 = 0, int sw2 = 0, int krtrd = 0, }) { final frame = Uint8List(128); // Header frame[0] = 0x1B; frame[1] = 0x01; // RPM: bytes 2..3, little-endian frame[2] = rpm & 0xFF; frame[3] = (rpm >> 8) & 0xFF; // VSS: byte 4 frame[4] = vss & 0xFF; // TPS: byte 5 frame[5] = tps & 0xFF; // MAP1: byte 6 frame[6] = map1 & 0xFF; // MAP2: bytes 33 (L), 34 (H) frame[33] = map2 & 0xFF; frame[34] = (map2 >> 8) & 0xFF; // IGN: byte 13 frame[13] = ign; // KRtrd: byte 21 frame[21] = krtrd; // SW1: byte 31, SW2: byte 32 frame[31] = sw1; frame[32] = sw2; // ECT: byte 49 frame[49] = ect; // BAT: byte 52 frame[51] = bat; // KMGR: BAT is byte 51 (the PDF says 52) // Stamp NEG8 checksum at byte 127 int neg8 = 0; for (int i = 0; i < 127; i++) { neg8 = (neg8 - frame[i]) & 0xFF; } frame[127] = neg8; return frame; } void main() { group('KPro parser', () { test('parses RPM = raw / 4', () { // rpm raw = 4000 → 1000.0 revs final frame = _buildKProFrame(rpm: 4000); final state = parseKPro(frame); expect(state.rpm, equals(1000.0)); }); test('parses VSS directly', () { final frame = _buildKProFrame(vss: 120); final state = parseKPro(frame); expect(state.vss, equals(120.0)); }); test('parses TPS = (raw - 25) / 2.04', () { final frame = _buildKProFrame(tps: 163); // KMGR: 68 % final state = parseKPro(frame); expect(state.tps, closeTo(67.6, 0.1)); }); test('parses MAP from MAP1 when MAP2 = 0', () { final frame = _buildKProFrame(map1: 100, map2: 0); final state = parseKPro(frame); expect(state.map, equals(100.0)); }); test('parses MAP from MAP2 / 10 when MAP2 != 0', () { // map2 raw = 1000 → 100.0 kPa final frame = _buildKProFrame(map1: 50, map2: 1000); final state = parseKPro(frame); expect(state.map, equals(100.0)); }); test('parses ECT using temp lookup table', () { // ect raw = 0x80 = 128 → tempXlt[128]=73 → 33°C final frame = _buildKProFrame(ect: 0x80); final state = parseKPro(frame); expect(state.ect, equals((tempXlt[0x80] - 40).toDouble())); }); test('parses BAT = raw / 10', () { // bat raw = 130 → 13.0 V final frame = _buildKProFrame(bat: 130); final state = parseKPro(frame); expect(state.bat, equals(13.0)); }); test('MIL flag from SW2 bit3', () { final frame = _buildKProFrame(sw2: 0x08); // bit3 final state = parseKPro(frame); expect(state.mil, isTrue); }); test('real bench frame matches KPro Manager (7267 rpm capture)', () { // First 64 bytes logged at 00:40:25 on 2026-10-13, while KPro Manager // showed: RPM 7267, VSS 0, TPS 68 %, IGN 5°, S.TRIM 0 %, BAT 11.40 V, // PA 95.0 kPa, INJ 0.00 ms, SCS/FLR on, VTP on, VTS off, MIL on. const head = [ 0x1b, 0x01, 0x8d, 0x71, 0x00, 0xa3, 0x75, 0xff, // 0x28, 0x00, 0x4d, 0x00, 0x00, 0x8a, 0x00, 0xd2, 0xd9, 0xaf, 0xf6, 0x97, 0x80, 0x00, 0x03, 0x8b, 0x00, 0x00, 0x12, 0x0c, 0x07, 0x09, 0x00, 0x50, 0x09, 0xa0, 0x04, 0x01, 0x00, 0x00, 0xff, 0x00, 0x00, 0xbf, 0xa3, 0xbf, 0xe8, 0x10, 0xf0, 0xff, 0x8d, 0xd8, 0xd2, 0x72, 0xe8, 0x5f, 0xfe, 0x80, 0x04, 0x00, 0x00, 0x00, 0x65, ]; final frame = Uint8List(128)..setRange(0, head.length, head); frame[127] = calculateNeg8(frame.sublist(0, 127)); final s = parseKPro(frame); expect(s.rpm, closeTo(7267, 1)); expect(s.vss, 0); expect(s.tps.round(), 68); expect(s.ign, 5); expect(s.strim, 0); expect(s.bat, closeTo(11.4, 0.001)); expect(s.pa, 95); expect(s.inj, 0); expect(s.map, closeTo(118.4, 0.001)); // 2.5 psi boost expect(s.fuelPump, isTrue); // FLR expect(s.fanOut, isFalse); // FANC expect(s.vtp, isTrue); expect(s.vtec, isFalse); // VTS expect(s.mil, isTrue); }); test('knock flag when krtrd > 0', () { final frame = _buildKProFrame(krtrd: 3); final state = parseKPro(frame); expect(state.knock, isTrue); }); test('errBytes has 18 entries', () { final frame = _buildKProFrame(); final state = parseKPro(frame); expect(state.errBytes.length, equals(18)); }); test('throws on wrong frame length', () { expect(() => parseKPro(Uint8List(64)), throwsArgumentError); }); test('throws on bad checksum', () { final frame = _buildKProFrame(rpm: 4000); frame[127] = 0x00; // corrupt expect(() => parseKPro(frame), throwsArgumentError); }); }); }