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 errBytes = List.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); }