hondavert-dev/lib/core/protocol/s300_parser.dart
2026-07-28 02:31:02 +05:30

199 lines
5.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
bool hasS300TailRpmPayload(Uint8List frame) {
return _s300TailRpmRaw(frame) != null;
}
/// 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,
// 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 int? tailRpmRaw = _s300TailRpmRaw(frame);
final int rpmRaw = tailRpmRaw ?? _readUint16BE(frame, 2);
final double rpm = (tailRpmRaw != null ? 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<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();
// AIN0AIN7: 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);
}
int? _s300TailRpmRaw(Uint8List frame) {
if (!isS300PlaceholderFrame(frame)) return null;
for (final offset in const [82, 86]) {
final rpm = _readUint16LE(frame, offset);
if (rpm > 300 && rpm < 12000) return rpm;
}
return null;
}