hondavert-dev/lib/core/protocol/s300_parser.dart
metatroncubeswdev 9a56baef61 Rework BT poller and S300 decoding after client feedback
Client reported crashes, wrong/frozen data and that the app floods the
adapter MCU. See docs/FIX_2026-10-12.md for the full analysis.

- Poller: strictly one request outstanding; next request only after a valid
  128-byte reply or a 500 ms timeout, then a quiet period + buffer flush.
  Default 100 ms (10 Hz), minimum 80 ms. DTC clear/probe share the same lock.
- BtNotifier: filter the adapter's default (no-ECU-data) frame instead of
  holding last values; EcuDataStatus + status banner; auto-reconnect on link
  loss, stalls or long default-only runs (rate-limited). A reconnect still in
  flight when the user disconnects is discarded.
- S300 offsets corrected by column counting (trims, PA, ERR00-03, Eth, SW5,
  AF); INJ/AIN little-endian; fan = SW5 FANC only. Default-frame detection
  requires bytes 0-16 and 32-36.
- KPro RPM/MAP2/AIN little-endian (RPM still needs a bench check).
- Crash hardening: zone/Flutter/platform error handlers to an in-app log
  (copyable from Debug); permissions requested before listing devices.
- Remove temporary debug prints; graph redraws live; polling options
  100-300 ms, persisted.

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

189 lines
7.7 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';
// ─────────────────────────────────────────────────────────────────────────────
// 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: raw byte. UNVERIFIED scale — shown as raw.
final double o2 = frame[16].toDouble();
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;
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,
errBytes: errBytes,
timestamp: DateTime.now(),
);
}
int _u16le(Uint8List frame, int offset) =>
frame[offset] | (frame[offset + 1] << 8);