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