Checked against the USB tool on the bench (3210 rpm): - O2: raw x 5/256 V (raw 195 -> 3.81 V, as the USB tool shows). - New derived sensor: injector duty = INJ ms x RPM / 1200 (16.67 ms @ 3210 rpm -> 44 %, as the USB tool shows). - Default dashboard flags: MIL, Fuel Pump, FAN Out, VTEC Out (the requirement's "FuelCut" is the fuel pump flag on the S300). - Log one raw frame (bytes 0-63) every 5 s so USB-tool readings can be matched to bytes; INJ, IGN, TPS, MIL, AF and PA still need that check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
4.6 KiB
Dart
156 lines
4.6 KiB
Dart
import 'dart:typed_data';
|
||
import 'sensor_state.dart';
|
||
import 'temp_table.dart';
|
||
import 'neg8.dart';
|
||
import 'dtc_map.dart';
|
||
|
||
/// 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');
|
||
}
|
||
|
||
// RPM: bytes 2..3 — raw / 4. Little-endian like every other multi-byte
|
||
// field from this adapter (S300 RPM/VSS/MAP confirmed LE live; the KPro
|
||
// table itself labels MAP2 and AFCMD as L/H byte pairs). Was read BE.
|
||
// VERIFY on bench: 2000 rpm must show 2000.
|
||
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 direct %
|
||
final double tps = frame[5].toDouble();
|
||
|
||
// 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: byte 11 raw
|
||
final double inj = frame[11].toDouble();
|
||
|
||
// IGN: byte 13 raw degrees
|
||
final double ign = frame[13].toDouble();
|
||
|
||
// 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 51
|
||
final double pa = frame[51].toDouble();
|
||
|
||
// BAT: byte 52 — raw / 10
|
||
final double bat = frame[52] / 10.0;
|
||
|
||
// STRIM: byte 32 (SW2 shares offset 32, STRIM is byte 20 per spec offset +20)
|
||
final double strim = frame[20].toDouble();
|
||
|
||
// 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
|
||
// SW2 bit2 = MIL
|
||
final bool mil = (sw2 & 0x04) != 0;
|
||
// SW1 bit6 = FLR. Unverified: never live-tested for KPro, and the
|
||
// equivalent S300 bit turned out to be fuel PUMP relay status, not fuel
|
||
// cut, despite an initially plausible-looking name — treat "FLR" the
|
||
// same way until confirmed.
|
||
final bool fuelPump = (sw1 & 0x40) != 0;
|
||
// SW1 bit0 = FANC
|
||
final bool fanOut = (sw1 & 0x01) != 0;
|
||
// SW2 bit1..0 = VTS (VTEC)
|
||
final bool vtec = (sw2 & 0x03) != 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,
|
||
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);
|
||
}
|