hondavert-dev/lib/core/protocol/kpro_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

151 lines
4.4 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';
/// Parses a 128-byte KPro ECU response frame into a [SensorState].
/// Throws [ArgumentError] if frame length is wrong or NEG8 checksum fails.
SensorState parseKPro(Uint8List frame) {
if (frame.length != 128) {
throw ArgumentError('KPro frame must be 128 bytes, got ${frame.length}');
}
if (!validateFrame(frame)) {
throw ArgumentError('KPro frame NEG8 checksum failed');
}
// RPM: bytes 2..3 — raw / 4. Little-endian like every other multi-byte
// field from this adapter (S300 RPM/VSS/MAP confirmed LE live; the KPro
// table itself labels MAP2 and AFCMD as L/H byte pairs). Was read BE.
// VERIFY on bench: 2000 rpm must show 2000.
final int rpmRaw = _readUint16LE(frame, 2);
final double rpm = rpmRaw / 4.0;
// VSS: byte 4 direct km/h
final double vss = frame[4].toDouble();
// TPS: byte 5 direct %
final double tps = frame[5].toDouble();
// MAP: bytes 6 (MAP1) and 33..34 (MAP2 L+H)
final int map1Raw = frame[6];
final int map2Raw = _readUint16LE(frame, 33); // MAP2L @33, MAP2H @34
final double map = (map2Raw != 0) ? map2Raw / 10.0 : map1Raw.toDouble();
// INJ: byte 11 raw
final double inj = frame[11].toDouble();
// IGN: byte 13 raw degrees
final double ign = frame[13].toDouble();
// O2: byte 14 raw
final double o2 = frame[14].toDouble();
// KRtrd: byte 21
final int krtrd = frame[21];
// SW bitmaps
final int sw1 = frame[31];
final int sw2 = frame[32];
// ECT: byte 49. Table is tempXlt[raw] - 40, not +40 — see s300_parser.dart
// for the reasoning (the declared -40..150 degC sensor range is only
// reachable with subtraction).
final double ect = (tempXlt[frame[49]] - 40).toDouble();
// IAT: byte 50
final double iat = (tempXlt[frame[50]] - 40).toDouble();
// PA: byte 51
final double pa = frame[51].toDouble();
// BAT: byte 52 — raw / 10
final double bat = frame[52] / 10.0;
// STRIM: byte 32 (SW2 shares offset 32, STRIM is byte 20 per spec offset +20)
final double strim = frame[20].toDouble();
// LTRIM — not explicitly listed in KPro; use 0
const double ltrim = 0.0;
// ETH — not in KPro spec; use 0
const double eth = 0.0;
// AFR: byte 25 (AFCMD at offset +19, then raw)
final double afr = frame[25].toDouble();
// ERR bytes: 18 bytes starting at offset 0x3E = 62
final List<int> errBytes = List<int>.generate(18, (i) => frame[62 + i]);
// AIN0–AIN7: bytes 0x52..0x61 (offset 82..97, uint16 LE each)
final double ain0 = _readUint16LE(frame, 82).toDouble();
final double ain1 = _readUint16LE(frame, 84).toDouble();
final double ain2 = _readUint16LE(frame, 86).toDouble();
final double ain3 = _readUint16LE(frame, 88).toDouble();
final double ain4 = _readUint16LE(frame, 90).toDouble();
final double ain5 = _readUint16LE(frame, 92).toDouble();
final double ain6 = _readUint16LE(frame, 94).toDouble();
final double ain7 = _readUint16LE(frame, 96).toDouble();
// Gear — not in KPro frame, default 0
const int gear = 0;
// Flags
// SW2 bit2 = MIL
final bool mil = (sw2 & 0x04) != 0;
// SW1 bit6 = FLR. Unverified: never live-tested for KPro, and the
// equivalent S300 bit turned out to be fuel PUMP relay status, not fuel
// cut, despite an initially plausible-looking name — treat "FLR" the
// same way until confirmed.
final bool fuelPump = (sw1 & 0x40) != 0;
// SW1 bit0 = FANC
final bool fanOut = (sw1 & 0x01) != 0;
// SW2 bit1..0 = VTS (VTEC)
final bool vtec = (sw2 & 0x03) != 0;
// Knock = KRtrd > 0
final bool knock = krtrd > 0;
// Rev limit — not directly mapped in KPro SW; false
const bool revLimit = false;
// Launch — not in KPro SW; false
const bool launch = false;
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 _readUint16LE(Uint8List frame, int offset) {
return frame[offset] | (frame[offset + 1] << 8);
}