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>
214 lines
8.7 KiB
Dart
214 lines
8.7 KiB
Dart
import 'dart:typed_data';
|
||
import 'sensor_state.dart';
|
||
import 'temp_table.dart';
|
||
import 'neg8.dart';
|
||
import 'dtc_map.dart';
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// S300 datalog frame (128 bytes, response to request 1B 00 E5).
|
||
//
|
||
// BYTE OFFSETS: hvbt_protocol.pdf labels its columns inconsistently
|
||
// ("+26 +27 +28 +29 +2A … +2F +30 +31"). The labels are NOT hex offsets.
|
||
// The real offsets are simply the column count, 0..127. This is proven by
|
||
// the fields already confirmed against live USB captures — Gear=27, ECT=33,
|
||
// IAT=34, BAT=36 — which all match plain column counting, and by the adapter's
|
||
// default frame (below) reproducing the PDF example row byte-for-byte.
|
||
//
|
||
// 0 ID1(1B) 1 ID2(00) 2 DLrate 3-4 RPM 5-6 VSS 7-8 MAP 9 TPS 10-11 INJ
|
||
// 12 IGN 13 KRtrd 14-15 Klvl 16 O2 17 SW1 18 SW2 19 SW3 20 SW4 21-26 ?
|
||
// 27 Gear 28 ? 29 Strim 30 ? 31 Ltrim 32 PA 33 ECT 34 IAT 35 ? 36 BAT
|
||
// 37 ERR00 38 ERR01 39 ERR02 40 ERR03 41 Eth 42-44 ? 45 IATc 46 ECTc 47-48 ?
|
||
// 49 SW5 50 SW6 51-54 ? 55 AF 56 ? 57 TCrtrd 58 TClim 59 ? 60-81 reserved
|
||
// 82-97 AIN0..AIN7 (16-bit) 98-126 reserved 127 NEG8
|
||
//
|
||
// BYTE ORDER: multi-byte values are LITTLE-endian on the wire (confirmed live
|
||
// for RPM/VSS/MAP; the PDF's "Big Endian" note only applies to the request).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// True when the frame is the adapter's built-in DEFAULT frame — the exact
|
||
/// example values printed in hvbt_protocol.pdf (DLrate 0x14, VSS 0xFFFF,
|
||
/// MAP 0x03F2, TPS 0x18, IGN 0x10, O2 0xC4 …; bytes 32-36 = 93 A0 A0 DC 7D).
|
||
/// The adapter sends it when it has no fresh data from the ECU (typically
|
||
/// because it was asked again before it could finish reading the ECU).
|
||
/// Such a frame carries NO real readings and must not be displayed or logged.
|
||
///
|
||
/// Both spans must match: bytes 0-16 alone could, in principle, coincide with
|
||
/// a real key-on/engine-off reading (RPM 0, MAP 101.0, closed throttle), and a
|
||
/// false positive here hides real data and triggers auto-reconnects.
|
||
bool isS300DefaultFrame(Uint8List frame) {
|
||
if (frame.length != 128) return false;
|
||
const head = <int>[
|
||
0x1B, 0x00, 0x14, 0x00, 0x00, 0xFF, 0xFF, 0xF2, 0x03, //
|
||
0x18, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xC4,
|
||
];
|
||
for (var i = 0; i < head.length; i++) {
|
||
if (frame[i] != head[i]) return false;
|
||
}
|
||
// PA, ECT, IAT, ?, BAT at 32-36.
|
||
const tail = <int>[0x93, 0xA0, 0xA0, 0xDC, 0x7D];
|
||
for (var i = 0; i < tail.length; i++) {
|
||
if (frame[32 + i] != tail[i]) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// Former name, kept so older call sites/tests keep compiling.
|
||
bool isS300TemplateFrame(Uint8List frame) => isS300DefaultFrame(frame);
|
||
|
||
/// Parses a 128-byte S300 ECU response frame into a [SensorState].
|
||
/// Throws [ArgumentError] if frame length is wrong, or if [validateChecksum]
|
||
/// is true and the NEG8 checksum fails.
|
||
SensorState parseS300(Uint8List frame, {bool validateChecksum = true}) {
|
||
if (frame.length != 128) {
|
||
throw ArgumentError('S300 frame must be 128 bytes, got ${frame.length}');
|
||
}
|
||
if (validateChecksum && !validateFrame(frame)) {
|
||
throw ArgumentError('S300 frame NEG8 checksum failed');
|
||
}
|
||
|
||
// ── Confirmed against live captures ────────────────────────────────────
|
||
final double rpm = _u16le(frame, 3).toDouble();
|
||
|
||
final int vssRaw = _u16le(frame, 5);
|
||
final double vss =
|
||
(vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 228480.0 / vssRaw;
|
||
|
||
final double map = _u16le(frame, 7) / 10.0;
|
||
|
||
final double tps = tpsPercent(frame[9]);
|
||
|
||
final int gear = frame[27];
|
||
|
||
// Table value minus 40 (live-confirmed; also the default frame's 0xA0
|
||
// decodes to a sensible 20 °C this way).
|
||
final double ect = (tempXlt[frame[33]] - 40).toDouble();
|
||
final double iat = (tempXlt[frame[34]] - 40).toDouble();
|
||
|
||
final double bat = frame[36] * 26.0 / 270.0;
|
||
|
||
// ── Offsets corrected 2026-10-12 (column counting, see header) ─────────
|
||
// Formulas marked UNVERIFIED still need one bench check against the USB
|
||
// tool; they are sanity-checked against the default frame only.
|
||
|
||
// INJ: 16-bit LE (was read big-endian). UNVERIFIED scale: assumes µs.
|
||
final double inj = _u16le(frame, 10) / 1000.0;
|
||
|
||
// IGN: formula from the PDF. UNVERIFIED (one live check disagreed).
|
||
final double ign = (frame[12] + 120) / 2.0;
|
||
|
||
// O2: volts, raw × 5/256 (USB tool: raw 195 → 3.81 V).
|
||
final double o2 = frame[16] * 5.0 / 256.0;
|
||
|
||
final int sw1 = frame[17];
|
||
final int sw2 = frame[18];
|
||
final int sw3 = frame[19];
|
||
final int sw5 = frame[49]; // was read from byte 67 (reserved area)
|
||
|
||
// Fuel trims: 0x80 = 0 %. Default frame has 0x80 in both → 0 %.
|
||
final double strim = (frame[29] - 128) * 100.0 / 128.0; // was byte 41
|
||
final double ltrim = (frame[31] - 128) * 100.0 / 128.0; // was byte 43
|
||
|
||
// Baro: byte 32 (was 44). Honda OBD1 baro ADC formula; default frame's
|
||
// 0x93 → 100.3 kPa (sea level), which supports it. UNVERIFIED on bench.
|
||
final double pa = frame[32] * 0.716 - 5.0;
|
||
|
||
// DTC bytes ERR00..ERR03 = 37..40 (were read from 49..52 = SW5/SW6/…,
|
||
// which produced phantom codes such as P0505/P0350 from SW6 = 0x60).
|
||
final List<int> errBytes = [frame[37], frame[38], frame[39], frame[40]];
|
||
|
||
// Ethanol %: byte 41 (was 56).
|
||
final double eth = frame[41].toDouble();
|
||
|
||
// AF: byte 55 (was 52 = unknown byte). UNVERIFIED scale — shown as raw.
|
||
final double afr = frame[55].toDouble();
|
||
|
||
// AIN0–AIN7: 82..97, 16-bit LE (was big-endian). Raw counts.
|
||
final double ain0 = _u16le(frame, 82).toDouble();
|
||
final double ain1 = _u16le(frame, 84).toDouble();
|
||
final double ain2 = _u16le(frame, 86).toDouble();
|
||
final double ain3 = _u16le(frame, 88).toDouble();
|
||
final double ain4 = _u16le(frame, 90).toDouble();
|
||
final double ain5 = _u16le(frame, 92).toDouble();
|
||
final double ain6 = _u16le(frame, 94).toDouble();
|
||
final double ain7 = _u16le(frame, 96).toDouble();
|
||
|
||
// ── Flags (bit maps from the PDF) ──────────────────────────────────────
|
||
// SW1: ?(7) DistErr(6) SCS(5) PSP(4) REVL(3) BKSW(2) ACSW(1) VTP(0)
|
||
final bool revLimit = (sw1 & 0x08) != 0;
|
||
// SW2: ALTC(7) IAB(6) MIL(5) PCS(4) ACCL(3) Fuel(2) VTS(1..0)
|
||
final bool mil = (sw2 & 0x20) != 0;
|
||
// Fuel pump relay, active-LOW (live-confirmed).
|
||
final bool fuelPump = (sw2 & 0x04) == 0;
|
||
final bool vtec = (sw2 & 0x03) != 0;
|
||
// SW5: … CL(2) … FANC(0). Fan output is FANC only — SW2 bit7 is ALTC
|
||
// (alternator control), not the fan.
|
||
final bool fanOut = (sw5 & 0x01) != 0;
|
||
final bool knock = frame[13] > 0; // KRtrd
|
||
// SW3: LnchC(7) LnchR(6) IGNC(5) ShftC(4) BstC(3)
|
||
final bool launch = (sw3 & 0x80) != 0;
|
||
final bool launchRetard = (sw3 & 0x40) != 0;
|
||
final bool ignCut = (sw3 & 0x20) != 0;
|
||
final bool shiftCut = (sw3 & 0x10) != 0;
|
||
final bool boostCut = (sw3 & 0x08) != 0;
|
||
final bool closedLoop = (sw5 & 0x04) != 0; // SW5 CL
|
||
final bool brakeSwitch = (sw1 & 0x04) != 0; // SW1 BKSW
|
||
final bool acSwitch = (sw1 & 0x02) != 0; // SW1 ACSW
|
||
final bool vtp = (sw1 & 0x01) != 0; // SW1 VTP (VTEC pressure)
|
||
|
||
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,
|
||
launchRetard: launchRetard,
|
||
ignCut: ignCut,
|
||
shiftCut: shiftCut,
|
||
boostCut: boostCut,
|
||
closedLoop: closedLoop,
|
||
brakeSwitch: brakeSwitch,
|
||
acSwitch: acSwitch,
|
||
vtp: vtp,
|
||
iatError: anyDtcBit(errBytes, s300IatErrorBits),
|
||
ectError: anyDtcBit(errBytes, s300EctErrorBits),
|
||
o2Error: anyDtcBit(errBytes, s300O2ErrorBits),
|
||
errBytes: errBytes,
|
||
timestamp: DateTime.now(),
|
||
);
|
||
}
|
||
|
||
/// Throttle %, both ECUs: (raw - 25) / 2.04, clamped to 0..100 (the PDF's
|
||
/// "TPS < 25 ? 0" threshold). Checked against the USB tools: S300 raw ≈ 88
|
||
/// -> 31 %, KPro raw 163 -> 68 %. (The previous raw × 100/255 matched only
|
||
/// near 50 %.)
|
||
double tpsPercent(int raw) => ((raw - 25) / 2.04).clamp(0.0, 100.0);
|
||
|
||
int _u16le(Uint8List frame, int offset) =>
|
||
frame[offset] | (frame[offset + 1] << 8);
|