import 'dart:typed_data'; import 'sensor_state.dart'; import 'temp_table.dart'; import 'neg8.dart'; bool isS300PlaceholderFrame(Uint8List frame) { if (frame.length != 128) return false; // Diagnostic signature observed in the S300 Bluetooth stream. Do not use // this to drop frames globally; current captures show real changing data can // still be carried in frames with this prefix. 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'); } // Some S300 Bluetooth frames carry live RPM in the tail payload at 82..83, // while the header bytes stay fixed at 1b 00 14 00 and decode to a bogus // 4864 rpm. Prefer the tail value when it looks like real engine speed. final bool hasTailPayload = _hasS300TailPayload(frame); final int rpmRaw = hasTailPayload ? _readUint16LE(frame, 82) : _readUint16BE(frame, 2); final double rpm = (hasTailPayload ? rpmRaw - 1089 : rpmRaw - 256) .clamp(0, 12000) .toDouble(); // VSS: bytes 5..6 little-endian pulse period. final int vssRaw = _readUint16LE(frame, 5); final double vss = (vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 144256.0 / vssRaw; // MAP: bytes 7..8 little-endian. SManager displays this as gauge pressure // in its "/psi column rather than raw absolute kPa. final int mapRaw = _readUint16LE(frame, 7); final double map = ((mapRaw / 10.0) - 103.8) * 0.1450377377; // TPS: byte 9 final int tpsRaw = frame[9]; final double tps = ((tpsRaw - 25) * 100.0 / (239 - 25)).clamp(0.0, 100.0); // INJ: bytes 10..11 little-endian timer ticks. final int injRaw = _readUint16LE(frame, 10); final double inj = injRaw / 240.5; // IGN: byte 12 — SManager-aligned degrees. final double ign = (frame[12] - 66) / 2.0; // O2: byte 16, 0..5V scaled over 8-bit ADC range. final double o2 = frame[16] * 5.0 / 256.0; // 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]; // +67 // Gear: byte 0x27 = +39 decimal... wait, +27 hex = decimal 39 final int gear = frame[0x27]; // Strim: byte 0x29 hex = 41 decimal final double strim = frame[0x29].toDouble(); // Ltrim: byte 0x2B hex = 43 decimal final double ltrim = frame[0x2B].toDouble(); // PA: byte 0x3C hex = 60 decimal final double pa = frame[0x3C].toDouble(); // ECT: byte 0x2D hex = 45 decimal final double ect = (tempXlt[frame[0x2D]] + 40).toDouble(); // IAT: byte 0x2E hex = 46 decimal final double iat = (tempXlt[frame[0x2E]] + 40).toDouble(); // BAT: byte 0x24 hex = 36 decimal final double bat = frame[0x24] * 26.0 / 270.0; // ERR bytes: 0x31..0x34 (4 bytes for S300) final List errBytes = [ frame[0x31], frame[0x32], frame[0x33], frame[0x34], ]; // Eth: byte 0x38 hex = 56 decimal final double eth = frame[0x38].toDouble(); // AFR: byte 0x34 overlaps with ERR03 in the spec — use it as raw AFR 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); } bool _hasS300TailPayload(Uint8List frame) { if (!isS300PlaceholderFrame(frame)) return false; final rpm = _readUint16LE(frame, 82); return rpm > 300 && rpm < 12000; }