192 lines
5.8 KiB
Dart
192 lines
5.8 KiB
Dart
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 : 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<int> 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;
|
||
}
|