KPro, checked against KPro Manager on the bench (7267 rpm capture; a test replays that frame): - TPS (raw - 25) / 2.04 (68 %), IGN (raw - 128) / 2 (5 deg), short trim (raw - 128) / 1.28, injector 16-bit LE at 11-12. - BAT is byte 51 / 10 (11.40 V) and PA byte 53 kPa (95.0); the PDF table had them at 52 / 51. - SW1/SW2 bits are listed lowest-first: FLR bit6, FANC bit7, VTP bit0, VTS bit1, MIL bit3 (VTEC Out no longer shows VTP). S300: the same TPS formula matches the USB tool (31 % at raw ~88). RPM glitch filter: frames whose RPM jumps more than an engine can are dropped (KPro bench crank fault P0336 sent ~300 rpm frames between real ones); a real change is accepted once 6 of the last 10 readings agree. Dropped count shown on the Debug screen. Connecting from Settings now replaces the whole navigation stack, so no stale dashboard is left underneath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
5.2 KiB
Dart
164 lines
5.2 KiB
Dart
import 'dart:typed_data';
|
||
import 'sensor_state.dart';
|
||
import 'temp_table.dart';
|
||
import 'neg8.dart';
|
||
import 'dtc_map.dart';
|
||
import 's300_parser.dart' show tpsPercent;
|
||
|
||
/// Parses a 128-byte KPro ECU response frame into a [SensorState].
|
||
/// Throws [ArgumentError] if frame length is wrong or NEG8 checksum fails.
|
||
SensorState parseKPro(Uint8List frame) {
|
||
if (frame.length != 128) {
|
||
throw ArgumentError('KPro frame must be 128 bytes, got ${frame.length}');
|
||
}
|
||
if (!validateFrame(frame)) {
|
||
throw ArgumentError('KPro frame NEG8 checksum failed');
|
||
}
|
||
|
||
// Values marked KMGR were checked against KPro Manager on the bench
|
||
// (2026-10-13, 7267 rpm frame); the PDF table's offsets for PA and BAT
|
||
// turned out to be wrong, see below.
|
||
|
||
// RPM: bytes 2..3 little-endian / 4. KMGR: 7267 rpm.
|
||
final int rpmRaw = _readUint16LE(frame, 2);
|
||
final double rpm = rpmRaw / 4.0;
|
||
|
||
// VSS: byte 4 direct km/h
|
||
final double vss = frame[4].toDouble();
|
||
|
||
// TPS: byte 5, (raw - 25) / 2.04. KMGR: raw 163 -> 68 %.
|
||
final double tps = tpsPercent(frame[5]);
|
||
|
||
// MAP: bytes 6 (MAP1) and 33..34 (MAP2 L+H)
|
||
final int map1Raw = frame[6];
|
||
final int map2Raw = _readUint16LE(frame, 33); // MAP2L @33, MAP2H @34
|
||
final double map = (map2Raw != 0) ? map2Raw / 10.0 : map1Raw.toDouble();
|
||
|
||
// INJ: bytes 11..12 little-endian, µs -> ms. KMGR: 00 00 -> 0.00 ms (in
|
||
// fuel cut). Byte order follows the L/H pairs; scale UNVERIFIED.
|
||
final double inj = _readUint16LE(frame, 11) / 1000.0;
|
||
|
||
// IGN: byte 13, (raw - 128) / 2 degrees. KMGR: raw 138 -> 5°.
|
||
final double ign = (frame[13] - 128) / 2.0;
|
||
|
||
// O2: byte 14 raw
|
||
// O2: volts, raw × 5/256 as on the S300. UNVERIFIED for KPro.
|
||
final double o2 = frame[14] * 5.0 / 256.0;
|
||
|
||
// KRtrd: byte 21
|
||
final int krtrd = frame[21];
|
||
|
||
// SW bitmaps
|
||
final int sw1 = frame[31];
|
||
final int sw2 = frame[32];
|
||
|
||
// ECT: byte 49. Table is tempXlt[raw] - 40, not +40 — see s300_parser.dart
|
||
// for the reasoning (the declared -40..150 degC sensor range is only
|
||
// reachable with subtraction).
|
||
final double ect = (tempXlt[frame[49]] - 40).toDouble();
|
||
|
||
// IAT: byte 50
|
||
final double iat = (tempXlt[frame[50]] - 40).toDouble();
|
||
|
||
// PA: byte 53, kPa. KMGR: raw 95 -> 95.0 kPa. (The PDF puts PA at 51.)
|
||
final double pa = frame[53].toDouble();
|
||
|
||
// BAT: byte 51, raw / 10. KMGR: raw 114 -> 11.40 V. (The PDF puts BAT at
|
||
// 52; that byte read 232 -> "23.2 V".)
|
||
final double bat = frame[51] / 10.0;
|
||
|
||
// STRIM: byte 20, (raw - 128) / 1.28 %. KMGR: raw 128 -> 0 %.
|
||
final double strim = (frame[20] - 128) / 1.28;
|
||
|
||
// LTRIM — not explicitly listed in KPro; use 0
|
||
const double ltrim = 0.0;
|
||
|
||
// ETH — not in KPro spec; use 0
|
||
const double eth = 0.0;
|
||
|
||
// AFR: byte 25 (AFCMD at offset +19, then raw)
|
||
final double afr = frame[25].toDouble();
|
||
|
||
// ERR bytes: 18 bytes starting at offset 0x3E = 62
|
||
final List<int> errBytes = List<int>.generate(18, (i) => frame[62 + i]);
|
||
|
||
// AIN0–AIN7: bytes 0x52..0x61 (offset 82..97, uint16 LE each)
|
||
final double ain0 = _readUint16LE(frame, 82).toDouble();
|
||
final double ain1 = _readUint16LE(frame, 84).toDouble();
|
||
final double ain2 = _readUint16LE(frame, 86).toDouble();
|
||
final double ain3 = _readUint16LE(frame, 88).toDouble();
|
||
final double ain4 = _readUint16LE(frame, 90).toDouble();
|
||
final double ain5 = _readUint16LE(frame, 92).toDouble();
|
||
final double ain6 = _readUint16LE(frame, 94).toDouble();
|
||
final double ain7 = _readUint16LE(frame, 96).toDouble();
|
||
|
||
// Gear — not in KPro frame, default 0
|
||
const int gear = 0;
|
||
|
||
// Flags. The PDF lists the bits LOWEST first. KMGR: SW1 = 0x50 with SCS
|
||
// and FLR on -> SCS bit4, FLR bit6.
|
||
// SW1: RVSLCK(0) BKSW(1) ACSW(2) ACCL(3) SCS(4) EPS(5) FLR(6) FANC(7)
|
||
final bool brakeSwitch = (sw1 & 0x02) != 0;
|
||
final bool acSwitch = (sw1 & 0x04) != 0;
|
||
final bool fuelPump = (sw1 & 0x40) != 0; // FLR = fuel pump relay
|
||
final bool fanOut = (sw1 & 0x80) != 0; // FANC
|
||
// SW2: VTP(0) VTS(1) … MIL(3). KMGR: SW2 = 0x01/0x09 with VTP on, VTS
|
||
// off, MIL on -> MIL is bit3 (the PDF list has a gap). MIL UNVERIFIED
|
||
// beyond this one capture.
|
||
final bool vtp = (sw2 & 0x01) != 0;
|
||
final bool vtec = (sw2 & 0x02) != 0; // VTS = VTEC solenoid
|
||
final bool mil = (sw2 & 0x08) != 0;
|
||
// Knock = KRtrd > 0
|
||
final bool knock = krtrd > 0;
|
||
// Rev limit — not directly mapped in KPro SW; false
|
||
const bool revLimit = false;
|
||
// Launch — not in KPro SW; false
|
||
const bool launch = false;
|
||
|
||
return SensorState(
|
||
rpm: rpm,
|
||
vss: vss,
|
||
map: map,
|
||
tps: tps,
|
||
inj: inj,
|
||
ign: ign,
|
||
ect: ect,
|
||
iat: iat,
|
||
bat: bat,
|
||
o2: o2,
|
||
gear: gear,
|
||
eth: eth,
|
||
pa: pa,
|
||
afr: afr,
|
||
strim: strim,
|
||
ltrim: ltrim,
|
||
ain0: ain0,
|
||
ain1: ain1,
|
||
ain2: ain2,
|
||
ain3: ain3,
|
||
ain4: ain4,
|
||
ain5: ain5,
|
||
ain6: ain6,
|
||
ain7: ain7,
|
||
mil: mil,
|
||
fuelPump: fuelPump,
|
||
fanOut: fanOut,
|
||
vtec: vtec,
|
||
knock: knock,
|
||
revLimit: revLimit,
|
||
launch: launch,
|
||
brakeSwitch: brakeSwitch,
|
||
acSwitch: acSwitch,
|
||
vtp: vtp,
|
||
iatError: anyDtcBit(errBytes, kproIatErrorBits),
|
||
ectError: anyDtcBit(errBytes, kproEctErrorBits),
|
||
o2Error: anyDtcBit(errBytes, kproO2ErrorBits),
|
||
errBytes: errBytes,
|
||
timestamp: DateTime.now(),
|
||
);
|
||
}
|
||
|
||
int _readUint16LE(Uint8List frame, int offset) {
|
||
return frame[offset] | (frame[offset + 1] << 8);
|
||
}
|