import 'dart:typed_data'; import 'sensor_state.dart'; import 'temp_table.dart'; import 'neg8.dart'; /// 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 final double pa = frame[0x2C].toDouble(); // ECT: +2D hex = 45 decimal. 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. The // written "+40" formula can only ever produce 40..230 degC, which // contradicts that declared range and explains the ~80 degC-too-hot // readings seen against real hardware. final double ect = (tempXlt[frame[0x2D]] - 40).toDouble(); // IAT: +2E hex = 46 decimal final double iat = (tempXlt[frame[0x2E]] - 40).toDouble(); // BAT: +30 hex = 48 decimal final double bat = frame[0x30] * 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 (cut) final bool fuelCut = (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, fuelCut: fuelCut, 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); }