hondavert-dev/test/s300_parser_test.dart
2026-08-08 13:18:02 -04:00

215 lines
6.1 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 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: byte +2D
frame[0x2D] = ect;
// BAT: byte +30
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 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', () {
// ect raw = 0x80 = 128 → tempXlt[128]=73 → 73-40=33°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 = 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('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 old "keep-alive" byte prefix', () {
// A fixed 17-byte prefix here was previously treated as a keep-alive
// signature to skip outright. Live testing showed it over-matched
// badly: with RPM actively changing, nearly every frame was being
// classified as a keep-alive and discarded, freezing the dashboard.
// Every checksum-valid frame is decoded normally now regardless of
// its leading bytes — see sensor_provider.dart.
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));
final state = parseS300(frame);
expect(state.rpm, equals(0.0));
});
});
}