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