- Phase 1 core protocol: temp_table, neg8, sensor_state, s300_parser, kpro_parser, dtc_map, sensor_defs (50/50 tests passing) - BT layer: bt_service.dart, bt_poller.dart (100ms poll, NEG8 validation) - Connection test UI: device picker, protocol selector, live sensor screen with LIVE DATA + DEBUG LOG tabs - Runtime BT permission request (Android 12+) + auto-enable Bluetooth - Android: minSdk=26, all BT+location permissions in manifest - Fixed flutter_bluetooth_serial namespace for AGP compatibility
156 lines
4.2 KiB
Dart
156 lines
4.2 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|