hondavert-dev/lib/core/protocol/s300_parser.dart
metatroncubeswdev 82b1d822f9 KPro values from KPro Manager, TPS formula, RPM glitch filter, nav fix
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>
2026-10-13 01:16:53 -04:00

214 lines
8.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);