hondavert-dev/lib/core/protocol/s300_parser.dart
metatroncubeswdev 6b8d069ef4 O2 in volts, injector duty, Fuel Pump default flag, raw frame log
Checked against the USB tool on the bench (3210 rpm):
- O2: raw x 5/256 V (raw 195 -> 3.81 V, as the USB tool shows).
- New derived sensor: injector duty = INJ ms x RPM / 1200
  (16.67 ms @ 3210 rpm -> 44 %, as the USB tool shows).
- Default dashboard flags: MIL, Fuel Pump, FAN Out, VTEC Out (the
  requirement's "FuelCut" is the fuel pump flag on the S300).
- Log one raw frame (bytes 0-63) every 5 s so USB-tool readings can be
  matched to bytes; INJ, IGN, TPS, MIL, AF and PA still need that check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:47:12 -04:00

209 lines
8.5 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';
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 = <int>[
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 = <int>[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;
// Linear 0-255 → 0-100 % (live-confirmed; the PDF formula exceeds 100 %).
final double tps = (frame[9] * 100.0 / 255.0).clamp(0.0, 100.0);
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<int> 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(),
);
}
int _u16le(Uint8List frame, int offset) =>
frame[offset] | (frame[offset + 1] << 8);