265 lines
8.5 KiB
Dart
265 lines
8.5 KiB
Dart
import 'dart:typed_data';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:hvbt_dash/core/protocol/neg8.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=33°C
|
|
int iat = 0x80,
|
|
int bat = 0,
|
|
int sw1 = 0,
|
|
int sw2 = 0,
|
|
int sw3 = 0,
|
|
int krtrd = 0,
|
|
int gear = 0,
|
|
}) {
|
|
final frame = Uint8List(128);
|
|
// Header
|
|
frame[0] = 0x1B;
|
|
frame[1] = 0x00;
|
|
// RPM: bytes +3..4, little-endian, raw
|
|
frame[3] = rpm & 0xFF;
|
|
frame[4] = (rpm >> 8) & 0xFF;
|
|
// VSS: bytes +5..6, little-endian
|
|
frame[5] = vss & 0xFF;
|
|
frame[6] = (vss >> 8) & 0xFF;
|
|
// MAP: bytes +7..8, little-endian
|
|
frame[7] = mapRaw & 0xFF;
|
|
frame[8] = (mapRaw >> 8) & 0xFF;
|
|
// TPS: byte +9
|
|
frame[9] = tps;
|
|
// INJ: bytes +10..11, BE
|
|
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;
|
|
// Gear: decimal offset 27
|
|
frame[27] = gear;
|
|
// ECT: decimal offset 33
|
|
frame[33] = ect;
|
|
// IAT: decimal offset 34
|
|
frame[34] = iat;
|
|
// BAT: decimal offset 36
|
|
frame[36] = 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 as raw/10 kPa', () {
|
|
// mapRaw = 1148 -> 114.8 kPa
|
|
final frame = _buildS300Frame(mapRaw: 1148);
|
|
final state = parseS300(frame);
|
|
expect(state.map, closeTo(114.8, 0.01));
|
|
});
|
|
|
|
test('parses TPS = 0 when raw = 0', () {
|
|
final frame = _buildS300Frame(tps: 0);
|
|
final state = parseS300(frame);
|
|
expect(state.tps, equals(0.0));
|
|
});
|
|
|
|
test('parses TPS = 100 when raw = 255', () {
|
|
final frame = _buildS300Frame(tps: 255);
|
|
final state = parseS300(frame);
|
|
expect(state.tps, equals(100.0));
|
|
});
|
|
|
|
test('parses TPS correctly when raw >= 25', () {
|
|
// tps raw=127 -> 127*100/255 ≈ 49.8% (confirmed against live capture)
|
|
final frame = _buildS300Frame(tps: 127);
|
|
final state = parseS300(frame);
|
|
expect(state.tps, closeTo(49.8, 0.1));
|
|
});
|
|
|
|
test('parses Gear from decimal offset 27', () {
|
|
final frame = _buildS300Frame(gear: 5);
|
|
final state = parseS300(frame);
|
|
expect(state.gear, equals(5));
|
|
});
|
|
|
|
test('parses ECT using temp lookup table', () {
|
|
// Confirmed via live capture: raw=0xD2(210) matched the reference
|
|
// tool's 28.4°F exactly at the same instant (RPM cross-checked).
|
|
// ect raw = 0xD2 = 210 → tempXlt[210]=38 → 38-40=-2°C
|
|
final frame = _buildS300Frame(ect: 0xD2);
|
|
final state = parseS300(frame);
|
|
expect(state.ect, equals((tempXlt[0xD2] - 40).toDouble()));
|
|
});
|
|
|
|
test('parses IAT using temp lookup table', () {
|
|
// Confirmed via the same live capture: raw=0xCA(202) was within
|
|
// ~1.3°F of the reference tool's 36.1°F reading.
|
|
// iat raw = 0xCA = 202 → tempXlt[202]=43 → 43-40=3°C
|
|
final frame = _buildS300Frame(iat: 0xCA);
|
|
final state = parseS300(frame);
|
|
expect(state.iat, equals((tempXlt[0xCA] - 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 = 164 -> (164 + 120) / 2 = 142.0
|
|
final frame = _buildS300Frame(ign: 164);
|
|
final state = parseS300(frame);
|
|
expect(state.ign, equals(142.0));
|
|
});
|
|
|
|
test('MIL flag set from SW2 bit5', () {
|
|
final frame = _buildS300Frame(sw2: 0x20); // bit5 set
|
|
final state = parseS300(frame);
|
|
expect(state.mil, isTrue);
|
|
});
|
|
|
|
test('fuel pump flag is active-low: set when SW2 bit2 is CLEAR', () {
|
|
// SW2 bit 2 is the "Fuel" line in the reference tool's own channel
|
|
// list — confirmed live to be the fuel PUMP relay status, not fuel
|
|
// cut, but also confirmed live to be active-low: an extended session
|
|
// held RPM above the ~300 threshold where the pump should be on, and
|
|
// this bit read 0 for the entire session. Separately confirmed the
|
|
// app showed "on" below ~200 rpm and "off" above it — exactly
|
|
// inverted from a naive `!= 0` read, hence bit CLEAR = pump on.
|
|
final frame = _buildS300Frame(sw2: 0x38); // bit2 clear, other bits set
|
|
final state = parseS300(frame);
|
|
expect(state.fuelPump, isTrue);
|
|
});
|
|
|
|
test('fuel pump flag clear (pump off) when SW2 bit2 is set', () {
|
|
final frame = _buildS300Frame(sw2: 0x04); // bit2 set
|
|
final state = parseS300(frame);
|
|
expect(state.fuelPump, isFalse);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
test('can parse bad checksum frame when validation is disabled', () {
|
|
final frame = _buildS300Frame(rpm: 1000);
|
|
frame[127] = 0x00; // corrupt checksum
|
|
final state = parseS300(frame, validateChecksum: false);
|
|
expect(state.rpm, equals(1000.0));
|
|
});
|
|
|
|
test('decodes RPM=0 for a frame matching the fixed event-frame template', () {
|
|
// parseS300 itself doesn't special-case this template — it just reads
|
|
// the normal offsets, which land on the template's fixed bytes.
|
|
// Holding the last known-good value across this frame is the caller's
|
|
// job (sensor_provider.dart), driven by isS300TemplateFrame below.
|
|
final frame = _templateFrame();
|
|
final state = parseS300(frame);
|
|
expect(state.rpm, equals(0.0));
|
|
});
|
|
|
|
test('isS300TemplateFrame detects the fixed event-frame byte pattern', () {
|
|
// Verified via live A/B capture: this exact bytes-0-16 pattern shows
|
|
// up whenever SW2 (byte 18, outside this span) reports fuel cut
|
|
// active, corroborated independently by the same frame's INJ pulse
|
|
// width reading 0 and TPS reading a closed-throttle value.
|
|
expect(isS300TemplateFrame(_templateFrame()), isTrue);
|
|
});
|
|
|
|
test('isS300TemplateFrame is false for a normal live-reading frame', () {
|
|
final frame = _buildS300Frame(rpm: 3000, tps: 127);
|
|
expect(isS300TemplateFrame(frame), isFalse);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Builds a 128-byte frame stamped with the S300's fixed event-frame
|
|
/// template in bytes 0-16, with a valid NEG8 checksum.
|
|
Uint8List _templateFrame() {
|
|
final frame = Uint8List(128);
|
|
final prefix = [
|
|
0x1B,
|
|
0x00,
|
|
0x14,
|
|
0x00,
|
|
0x00,
|
|
0xFF,
|
|
0xFF,
|
|
0xF2,
|
|
0x03,
|
|
0x18,
|
|
0x00,
|
|
0x00,
|
|
0x10,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0xC4,
|
|
];
|
|
for (var i = 0; i < prefix.length; i++) {
|
|
frame[i] = prefix[i];
|
|
}
|
|
frame[127] = calculateNeg8(frame.sublist(0, 127));
|
|
return frame;
|
|
}
|