import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:hvbt_dash/core/protocol/s300_parser.dart'; import 'package:hvbt_dash/core/protocol/temp_table.dart'; /// Builds a zeroed 128-byte S300 frame and stamps the NEG8 checksum at byte 127. Uint8List _buildS300Frame({ int rpm = 0, int vss = 0, int mapRaw = 0, int tps = 0, int inj = 0, int ign = 0, int ect = 0x80, // tempXlt[0x80]=73 → 73+40=113°C int bat = 0, int sw1 = 0, int sw2 = 0, int sw3 = 0, int krtrd = 0, }) { final frame = Uint8List(128); // Header frame[0] = 0x1B; frame[1] = 0x00; // RPM: bytes 3..4 frame[3] = (rpm >> 8) & 0xFF; frame[4] = rpm & 0xFF; // VSS: bytes 5..6 frame[5] = (vss >> 8) & 0xFF; frame[6] = vss & 0xFF; // MAP: bytes 7..8 frame[7] = (mapRaw >> 8) & 0xFF; frame[8] = mapRaw & 0xFF; // TPS: byte 9 frame[9] = tps; // INJ: bytes 10..11 frame[10] = (inj >> 8) & 0xFF; frame[11] = inj & 0xFF; // IGN: byte 12 frame[12] = ign; // KRtrd: byte 13 frame[13] = krtrd; // SW bitmaps frame[0x11] = sw1; frame[0x12] = sw2; frame[0x13] = sw3; // ECT: byte 0x2D frame[0x2D] = ect; // BAT: byte 0x30 frame[0x30] = bat; // 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('S300 parser', () { test('parses RPM correctly', () { // RPM raw = 3000 final frame = _buildS300Frame(rpm: 3000); final state = parseS300(frame); expect(state.rpm, equals(3000.0)); }); test('parses VSS = 0 when raw < 893', () { final frame = _buildS300Frame(vss: 500); final state = parseS300(frame); expect(state.vss, equals(0.0)); }); test('parses VSS correctly when raw >= 893', () { // vss = 228480 / 1000 = 228.48 km/h final frame = _buildS300Frame(vss: 1000); final state = parseS300(frame); expect(state.vss, closeTo(228.48, 0.01)); }); test('parses MAP = raw / 10', () { // mapRaw = 1000 → 100.0 kPa final frame = _buildS300Frame(mapRaw: 1000); final state = parseS300(frame); expect(state.map, equals(100.0)); }); test('parses TPS = 0 when raw < 25', () { final frame = _buildS300Frame(tps: 10); final state = parseS300(frame); expect(state.tps, equals(0.0)); }); test('parses TPS correctly when raw >= 25', () { // tps raw=46 → 46*51/46 = 51.0 final frame = _buildS300Frame(tps: 46); final state = parseS300(frame); expect(state.tps, closeTo(51.0, 0.01)); }); test('parses ECT using temp lookup table', () { // ect raw = 0x80 = 128 → tempXlt[128]=73 → 73+40=113°C final frame = _buildS300Frame(ect: 0x80); final state = parseS300(frame); expect(state.ect, equals((tempXlt[0x80] + 40).toDouble())); }); test('parses BAT = raw * 26 / 270', () { // bat raw = 135 → 135*26/270 = 13.0 V final frame = _buildS300Frame(bat: 135); final state = parseS300(frame); expect(state.bat, closeTo(13.0, 0.01)); }); test('IGN decode: (raw + 120) / 2', () { // ign raw = 20 → (20+120)/2 = 70.0° final frame = _buildS300Frame(ign: 20); final state = parseS300(frame); expect(state.ign, equals(70.0)); }); test('MIL flag set from SW2 bit5', () { final frame = _buildS300Frame(sw2: 0x20); // bit5 set final state = parseS300(frame); expect(state.mil, isTrue); }); test('knock flag set when krtrd > 0', () { final frame = _buildS300Frame(krtrd: 5); final state = parseS300(frame); expect(state.knock, isTrue); }); test('knock flag clear when krtrd = 0', () { final frame = _buildS300Frame(krtrd: 0); final state = parseS300(frame); expect(state.knock, isFalse); }); test('throws on wrong frame length', () { expect( () => parseS300(Uint8List(64)), throwsArgumentError, ); }); test('throws on bad checksum', () { final frame = _buildS300Frame(rpm: 1000); frame[127] = 0x00; // corrupt checksum expect(() => parseS300(frame), throwsArgumentError); }); }); }