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>
238 lines
9.3 KiB
Dart
238 lines
9.3 KiB
Dart
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);
|
||
}
|