Dashboard - Selectable main sensor (gauge) + sub sensor, 10 selectable values (landscape 5 left + 5 right, portrait 2x5 on one screen) and 4 selectable flags; long-press to change; persisted. - Flags: the requirement examples (MIL, FAN Out, VTEC Out, Knock, WBO / IAT / CLT Error - errors derived from the stored DTC bits) plus the other S300 status bits (cuts, launch, closed loop, VTP, AC, brake). - Record button moved to the app bar; debug screen moved to Settings. Sensor list: 1 column on phone portrait, 2 on tablet/landscape; name, unit, min/max, segmented bar and value. Graph: auto-scaled Y, window max/min, current value, seconds axis and a touch cursor; RPM/MAP/TPS/ECT by default. Units: km/h|mph, degC|degF, kPa|bar|psi (display only), persisted. Light/dark: palette is a ThemeExtension used by every screen; theme persisted. SafeArea keeps content off the side navigation bar. Datalog playback now drives the dashboard, list, graph and DTC screen, with the playback bar on the main screen and seek updating the displays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
4.6 KiB
Dart
155 lines
4.6 KiB
Dart
import 'dart:typed_data';
|
||
import 'sensor_state.dart';
|
||
import 'temp_table.dart';
|
||
import 'neg8.dart';
|
||
import 'dtc_map.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,
|
||
iatError: anyDtcBit(errBytes, kproIatErrorBits),
|
||
ectError: anyDtcBit(errBytes, kproEctErrorBits),
|
||
o2Error: anyDtcBit(errBytes, kproO2ErrorBits),
|
||
errBytes: errBytes,
|
||
timestamp: DateTime.now(),
|
||
);
|
||
}
|
||
|
||
int _readUint16LE(Uint8List frame, int offset) {
|
||
return frame[offset] | (frame[offset + 1] << 8);
|
||
}
|