hondavert-dev/lib/core/protocol/s300_parser.dart
metatroncubeswdev e9b1c0f8fd Fuel pump flag, confirmed ECT/IAT/BAT offsets, persisted ECU type
Work that was previously only in the local working tree:
- Rename fuelCut -> fuelPump (SW2 bit2, active-low, live-confirmed)
- ECT/IAT/BAT moved to live-confirmed offsets 33/34/36
- S300 template-frame detection with hold-last-good logic
- Persist ECU type in SharedPreferences; drop per-connect protocol dialog
- Official-protocol probe and DTC clear on the debug/DTC screens
- Sanity clamp for multi-byte fields; scrollable vertical dashboard
- docs/HANDOFF.md

Includes temporary [DBG]/[KDBG] prints, removed in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:02:05 -04:00

238 lines
9.3 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';
/// True when bytes 0-16 of the frame (header, RPM, VSS, MAP, TPS, INJ, IGN,
/// the knock byte, and O2) match a fixed pattern instead of a real reading,
/// with those fields pinned to fixed constants regardless of the true
/// physical signal.
///
/// The frozen region is now known to extend well past byte 16. Bytes 32-36
/// (gear, ECT, IAT, BAT at their current offsets) show the exact same fixed
/// byte sequence (`93 a0 a0 dc 7d`) across dozens of unrelated live captures
/// throughout this project. SW2's flags (fuelPump, mil, vtec, fanOut's ALTC
/// bit) were also seen going wrong during an idle period where nothing
/// changed on the bench but the rest of the frame was also frozen — the
/// earlier belief that SW2 stays live (based on an RPM sweep showing
/// fuelPump correlating with real changes) only holds while the frame
/// *isn't* templated; during a template frame it goes stale like everything
/// else tested so far. SW1 itself was never actually confirmed for template
/// frames specifically — the "matches USB exactly" validation that seemed
/// to support "bytes 17+ are live" was checked against a non-template frame.
/// At this point, don't assume any field is live during a template frame
/// unless it's been individually verified that way — the safer default is
/// that the whole frame is templated, with only a shrinking set of
/// individually-confirmed-live fields as exceptions (currently: none
/// confirmed with certainty — treat the entire byte-17+ region as suspect
/// until proven otherwise field by field).
///
/// Callers should hold the last known-good values for every field sourced
/// from the frozen span (bytes 0-16, gear/ECT/IAT/BAT, and the SW2 flags)
/// and decode only what's individually confirmed live.
bool isS300TemplateFrame(Uint8List frame) {
if (frame.length != 128) return false;
return frame[0] == 0x1B &&
frame[1] == 0x00 &&
frame[2] == 0x14 &&
frame[3] == 0x00 &&
frame[4] == 0x00 &&
frame[5] == 0xFF &&
frame[6] == 0xFF &&
frame[7] == 0xF2 &&
frame[8] == 0x03 &&
frame[9] == 0x18 &&
frame[10] == 0x00 &&
frame[11] == 0x00 &&
frame[12] == 0x10 &&
frame[13] == 0x00 &&
frame[14] == 0x00 &&
frame[15] == 0x00 &&
frame[16] == 0xC4;
}
/// 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');
}
// RPM: bytes +3..4, little-endian, use directly. Confirmed against a live
// capture: LE @ offset 3 matched a reference tool's RPM exactly, while
// REQUIREMENTS.md's blanket "all multi-byte values are Big Endian" claim
// did not hold for this field on real hardware.
final double rpm = _readUint16LE(frame, 3).toDouble();
// VSS: bytes +5..6, little-endian (confirmed against a live capture).
final int vssRaw = _readUint16LE(frame, 5);
final double vss =
(vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 228480.0 / vssRaw;
// MAP: bytes +7..8, little-endian (confirmed against a live capture — the
// big-endian read produced multi-thousand-kPa values, physically
// impossible for a 0-300 kPa sensor).
final int mapRaw = _readUint16LE(frame, 7);
final double map = mapRaw / 10.0;
// TPS: byte +9. Linear 0-255 -> 0-100%, confirmed against a live capture
// (raw 127 matched a reference tool's ~50% reading; the spec's
// "raw*51/46" formula produced impossible >100% values on the same byte).
final int tpsRaw = frame[9];
final double tps = (tpsRaw * 100.0 / 255.0).clamp(0.0, 100.0);
// INJ: bytes +10..11, BE, raw ms.
final double inj = _readUint16BE(frame, 10).toDouble();
// IGN: byte +12.
final double ign = (frame[12] + 120) / 2.0;
// O2: byte +16, raw.
final double o2 = frame[16].toDouble();
// SW bitmaps
final int sw1 = frame[0x11]; // +17
final int sw2 = frame[0x12]; // +18
final int sw3 = frame[0x13]; // +19
final int sw5 = frame[0x43]; // +43
// Gear: decimal offset 27 (confirmed against a live capture — hex 0x27/39
// read 0x00 there; decimal 27 matched a reference tool's gear exactly).
final int gear = frame[27];
// Strim: +29 hex = 41 decimal
final double strim = frame[0x29].toDouble();
// Ltrim: +2B hex = 43 decimal
final double ltrim = frame[0x2B].toDouble();
// PA: +2C hex = 44 decimal. Unconfirmed — a live capture's closest
// candidate (byte 50, decimal) was off by ~3 kPa from the reference
// tool's 98.7 kPa reading, not a clean enough match to move to yet.
final double pa = frame[0x2C].toDouble();
// ECT: decimal offset 33. Confirmed via live capture matching the
// reference tool exactly: raw=0xD2(210), tempXlt[210]=38, 38-40=-2°C =
// 28.4°F, matching the reference tool's 28.4°F reading precisely at the
// same instant (RPM cross-checked to confirm same-moment capture). The
// previous offset (+2D hex = 45 decimal) gave physically impossible
// values (~210°F) against the same live data. Table is tempXlt[raw]-40,
// not +40: the spec's own sensor range (section 3: ECT/IAT -40..150 degC)
// is only reachable with subtraction, since tempXlt entries span 0..190.
final double ect = (tempXlt[frame[33]] - 40).toDouble();
// IAT: decimal offset 34, immediately after ECT. Confirmed via the same
// live capture: raw=0xCA(202), tempXlt[202]=43, 43-40=3°C=37.4°F vs the
// reference tool's 36.1°F — within ~1.3°F, consistent with table
// granularity. The previous offset (+2E hex = 46 decimal) was part of the
// same wrong-offset block as the old ECT location.
final double iat = (tempXlt[frame[34]] - 40).toDouble();
// BAT: decimal offset 36. Confirmed via live capture: raw=0x79(121),
// 121*26/270=11.66V vs the reference tool's 11.75V — within one raw unit
// of quantization (26/270 V per LSB ≈ 0.096V). This is also where BAT
// lived before an earlier, unverified "fix" in this project's history
// moved it to +30 hex = 48 decimal — that fix was wrong; this restores
// the original, now confirmed-correct, offset.
final double bat = frame[36] * 26.0 / 270.0;
// ERR bytes: +31..+34 hex = 49..52 decimal
final List<int> errBytes = [
frame[0x31],
frame[0x32],
frame[0x33],
frame[0x34],
];
// Eth: +38 hex = 56 decimal
final double eth = frame[0x38].toDouble();
// AFR: spec overlaps +34 with ERR03; reuse that byte per the spec's own table.
final double afr = frame[0x34].toDouble();
// AIN0–AIN7: decimal offsets 82..97 (uint16 BE each)
final double ain0 = _readUint16BE(frame, 82).toDouble();
final double ain1 = _readUint16BE(frame, 84).toDouble();
final double ain2 = _readUint16BE(frame, 86).toDouble();
final double ain3 = _readUint16BE(frame, 88).toDouble();
final double ain4 = _readUint16BE(frame, 90).toDouble();
final double ain5 = _readUint16BE(frame, 92).toDouble();
final double ain6 = _readUint16BE(frame, 94).toDouble();
final double ain7 = _readUint16BE(frame, 96).toDouble();
// Flags
// SW1 bit3 = REVL
final bool revLimit = (sw1 & 0x08) != 0;
// SW2 bit5 = MIL
final bool mil = (sw2 & 0x20) != 0;
// SW2 bit2 = Fuel — this is the fuel PUMP relay status, not fuel cut (the
// reference tool's own channel list labels it as such). There's no
// separate "fuel cut" bit identified in this frame — REVL/IGNC/BstC/
// LnchC/ShftC are the actual cut-type flags this ECU exposes.
//
// Active-LOW: confirmed via an extended live session (RPM swept 400-3842,
// continuously above the ~300 threshold where the pump should be on) that
// this bit read 0 the entire time — and separately confirmed live that the
// app showed "on" below ~200 rpm (pump should be off) and "off" above the
// threshold (pump should be on), i.e. exactly inverted from a `!= 0` read.
// Bit clear (0) means the pump relay is energized/on; bit set (1) means off.
final bool fuelPump = (sw2 & 0x04) == 0;
// SW2 bit7 = ALTC → fanOut; also SW5 bit0 = FANC
final bool fanOut = (sw2 & 0x80) != 0 || (sw5 & 0x01) != 0;
// SW2 bit1..0 = VTS (VTEC solenoid — nonzero = active)
final bool vtec = (sw2 & 0x03) != 0;
// Knock = KRtrd > 0 (byte 13)
final bool knock = frame[13] > 0;
// SW3 bit7 = LnchC
final bool launch = (sw3 & 0x80) != 0;
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,
errBytes: errBytes,
timestamp: DateTime.now(),
);
}
int _readUint16BE(Uint8List frame, int offset) {
return (frame[offset] << 8) | frame[offset + 1];
}
int _readUint16LE(Uint8List frame, int offset) {
return frame[offset] | (frame[offset + 1] << 8);
}