From 77a47eb938d7ff98cdbab4f777454183cb0f75c2 Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Sat, 8 Aug 2026 13:18:02 -0400 Subject: [PATCH] the working code s300, with few glitch by resetting to 0 --- android/gradle.properties | 4 + lib/core/bluetooth/bt_poller.dart | 60 ++++-- lib/core/models/sensor_defs.dart | 8 +- lib/core/protocol/kpro_parser.dart | 8 +- lib/core/protocol/s300_parser.dart | 119 +++++------- lib/core/protocol/temp_table.dart | 4 +- lib/core/providers/sensor_provider.dart | 76 +++++++- .../screens/debug/raw_frame_debug_screen.dart | 70 ++++--- pubspec.lock | 8 +- test/kpro_parser_test.dart | 6 +- test/s300_parser_test.dart | 180 ++++++------------ test/temp_table_test.dart | 24 ++- 12 files changed, 295 insertions(+), 272 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index 282583b..ff4541f 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -2,3 +2,7 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=512m android.useAndroidX=true kotlin.incremental=false org.gradle.daemon=false +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/lib/core/bluetooth/bt_poller.dart b/lib/core/bluetooth/bt_poller.dart index c546799..235f1f9 100644 --- a/lib/core/bluetooth/bt_poller.dart +++ b/lib/core/bluetooth/bt_poller.dart @@ -31,6 +31,15 @@ class BtPoller { int droppedFrames = 0; int validFrames = 0; + // Tracks whether a request is still awaiting its response, so the poller + // never pipelines more than one request ahead of the device. Without this, + // a device that answers slower than [pollingInterval] falls permanently + // behind — every displayed frame ends up answering a stale, already-queued + // request instead of the most recent one, and the dashboard visibly lags + // real engine changes by a growing amount. + bool _awaitingResponse = false; + DateTime? _lastRequestSentAt; + BtPoller( this._service, { this.ecuType = EcuType.s300, @@ -39,6 +48,8 @@ class BtPoller { void start() { _rxBuf.clear(); + _awaitingResponse = false; + _lastRequestSentAt = null; // Listen to raw bytes from BT and accumulate into 128-byte frames _rxSub = _service.rawStream.listen( @@ -52,13 +63,28 @@ class BtPoller { }, ); - // Send request at polling interval - _pollTimer = Timer.periodic(pollingInterval, (_) async { - if (_service.isConnected) { - await _service.write( - ecuType == EcuType.s300 ? _s300Request : _kproRequest, - ); - } + // Check every pollingInterval whether it's time to send the next + // request — but only actually send once the prior one has been + // answered (or has clearly been lost). + _pollTimer = Timer.periodic(pollingInterval, (_) => _maybeSendRequest()); + _maybeSendRequest(); + } + + void _maybeSendRequest() { + if (!_service.isConnected) return; + + final stalled = _lastRequestSentAt != null && + DateTime.now().difference(_lastRequestSentAt!) > pollingInterval * 4; + if (_awaitingResponse && !stalled) return; + + _awaitingResponse = true; + _lastRequestSentAt = DateTime.now(); + _service + .write(ecuType == EcuType.s300 ? _s300Request : _kproRequest) + .catchError((Object _) { + // A failed write must not leave the poller permanently waiting for a + // response that was never actually requested — let the next tick retry. + _awaitingResponse = false; }); } @@ -88,15 +114,16 @@ class BtPoller { Uint8List.fromList(_rxBuf.sublist(startIdx, startIdx + 128)); final checksumOk = validateFrame(frame); - final canUseFrame = checksumOk || ecuType == EcuType.s300; - if (canUseFrame) { - if (!checksumOk) droppedFrames++; + if (checksumOk) { validFrames++; latestUsableFrame = frame; _rxBuf.removeRange(0, startIdx + 128); } else { - // Bad checksum — skip this byte and try again + // Bad checksum — the window is misaligned (or the frame was + // corrupted in transit). Drop one byte and rescan for the next + // 0x1B so the stream resynchronizes instead of permanently + // sliding a fixed-size window over garbage. droppedFrames++; _rxBuf.removeRange(0, startIdx + 1); } @@ -104,6 +131,17 @@ class BtPoller { if (latestUsableFrame != null) { _frameController.add(latestUsableFrame); + // The device answered — clear the road for the next request. Wait out + // the configured pollingInterval before sending it rather than firing + // immediately: back-to-back requests with no gap can outrun the ECU's + // own tach input-capture timing, which needs a moment to latch a fresh + // RPM count between reads — hammering it faster than that makes it + // fall back to reporting "no fresh count yet" on every subsequent + // request instead of a real reading. pollingInterval is also what the + // Settings screen exposes, so this makes that control actually pace + // the link instead of just being a stalled-request fallback. + _awaitingResponse = false; + Timer(pollingInterval, _maybeSendRequest); } } diff --git a/lib/core/models/sensor_defs.dart b/lib/core/models/sensor_defs.dart index d18128f..3620273 100644 --- a/lib/core/models/sensor_defs.dart +++ b/lib/core/models/sensor_defs.dart @@ -4,13 +4,13 @@ import 'flag_def.dart'; /// Master list of all analog sensor definitions. const List sensorDefs = [ SensorDef(id: 'rpm', displayName: 'RPM', unit: 'revs', min: 0, max: 9000), - SensorDef(id: 'vss', displayName: 'Speed', unit: 'mph', min: 0, max: 180), + SensorDef(id: 'vss', displayName: 'Speed', unit: 'km/h', min: 0, max: 280), SensorDef( id: 'map', displayName: 'Manifold pressure', - unit: 'psi', - min: -30, - max: 30), + unit: 'kPa', + min: 0, + max: 300), SensorDef( id: 'tps', displayName: 'Throttle pedal', unit: '%', min: 0, max: 100), SensorDef( diff --git a/lib/core/protocol/kpro_parser.dart b/lib/core/protocol/kpro_parser.dart index 19f4470..df6b6a9 100644 --- a/lib/core/protocol/kpro_parser.dart +++ b/lib/core/protocol/kpro_parser.dart @@ -44,11 +44,13 @@ SensorState parseKPro(Uint8List frame) { final int sw1 = frame[31]; final int sw2 = frame[32]; - // ECT: byte 49 - final double ect = (tempXlt[frame[49]] + 40).toDouble(); + // 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(); + final double iat = (tempXlt[frame[50]] - 40).toDouble(); // PA: byte 51 final double pa = frame[51].toDouble(); diff --git a/lib/core/protocol/s300_parser.dart b/lib/core/protocol/s300_parser.dart index 9d1615b..6ede90b 100644 --- a/lib/core/protocol/s300_parser.dart +++ b/lib/core/protocol/s300_parser.dart @@ -3,35 +3,6 @@ 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. @@ -43,67 +14,72 @@ SensorState parseS300(Uint8List frame, {bool validateChecksum = true}) { 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(); + // RPM: bytes +3..4, little-endian, use directly. Confirmed against a live + // capture: LE @ offset 3 matched a reference tool's RPM exactly, while + // REQUIREMENTS.md's blanket "all multi-byte values are Big Endian" claim + // did not hold for this field on real hardware. + final double rpm = _readUint16LE(frame, 3).toDouble(); - // VSS: bytes 5..6 little-endian pulse period. + // VSS: bytes +5..6, little-endian (confirmed against a live capture). final int vssRaw = _readUint16LE(frame, 5); final double vss = - (vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 144256.0 / vssRaw; + (vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 228480.0 / vssRaw; - // MAP: bytes 7..8 little-endian. SManager displays this as gauge pressure - // in its "/psi column rather than raw absolute kPa. + // MAP: bytes +7..8, little-endian (confirmed against a live capture — the + // big-endian read produced multi-thousand-kPa values, physically + // impossible for a 0-300 kPa sensor). final int mapRaw = _readUint16LE(frame, 7); - final double map = ((mapRaw / 10.0) - 103.8) * 0.1450377377; + final double map = mapRaw / 10.0; - // TPS: byte 9 + // TPS: byte +9. Linear 0-255 -> 0-100%, confirmed against a live capture + // (raw 127 matched a reference tool's ~50% reading; the spec's + // "raw*51/46" formula produced impossible >100% values on the same byte). final int tpsRaw = frame[9]; - final double tps = ((tpsRaw - 25) * 100.0 / (239 - 25)).clamp(0.0, 100.0); + final double tps = (tpsRaw * 100.0 / 255.0).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; + // INJ: bytes +10..11, BE, raw ms. + final double inj = _readUint16BE(frame, 10).toDouble(); - // IGN: byte 12 — SManager-aligned degrees. - final double ign = (frame[12] - 66) / 2.0; + // IGN: byte +12. + final double ign = (frame[12] + 120) / 2.0; - // O2: byte 16, 0..5V scaled over 8-bit ADC range. - final double o2 = frame[16] * 5.0 / 256.0; + // O2: byte +16, raw. + final double o2 = frame[16].toDouble(); // 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 + final int sw5 = frame[0x43]; // +43 - // Gear: byte 0x27 = +39 decimal... wait, +27 hex = decimal 39 - final int gear = frame[0x27]; + // Gear: decimal offset 27 (confirmed against a live capture — hex 0x27/39 + // read 0x00 there; decimal 27 matched a reference tool's gear exactly). + final int gear = frame[27]; - // Strim: byte 0x29 hex = 41 decimal + // Strim: +29 hex = 41 decimal final double strim = frame[0x29].toDouble(); - // Ltrim: byte 0x2B hex = 43 decimal + // Ltrim: +2B hex = 43 decimal final double ltrim = frame[0x2B].toDouble(); - // PA: byte 0x3C hex = 60 decimal - final double pa = frame[0x3C].toDouble(); + // PA: +2C hex = 44 decimal + final double pa = frame[0x2C].toDouble(); - // ECT: byte 0x2D hex = 45 decimal - final double ect = (tempXlt[frame[0x2D]] + 40).toDouble(); + // ECT: +2D hex = 45 decimal. Table is tempXlt[raw] - 40, not +40: the + // spec's own sensor range (section 3: ECT/IAT -40..150 degC) is only + // reachable with subtraction, since tempXlt entries span 0..190. The + // written "+40" formula can only ever produce 40..230 degC, which + // contradicts that declared range and explains the ~80 degC-too-hot + // readings seen against real hardware. + final double ect = (tempXlt[frame[0x2D]] - 40).toDouble(); - // IAT: byte 0x2E hex = 46 decimal - final double iat = (tempXlt[frame[0x2E]] + 40).toDouble(); + // IAT: +2E 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; + // BAT: +30 hex = 48 decimal + final double bat = frame[0x30] * 26.0 / 270.0; - // ERR bytes: 0x31..0x34 (4 bytes for S300) + // ERR bytes: +31..+34 hex = 49..52 decimal final List errBytes = [ frame[0x31], frame[0x32], @@ -111,10 +87,10 @@ SensorState parseS300(Uint8List frame, {bool validateChecksum = true}) { frame[0x34], ]; - // Eth: byte 0x38 hex = 56 decimal + // Eth: +38 hex = 56 decimal final double eth = frame[0x38].toDouble(); - // AFR: byte 0x34 overlaps with ERR03 in the spec — use it as raw AFR + // AFR: spec overlaps +34 with ERR03; reuse that byte per the spec's own table. final double afr = frame[0x34].toDouble(); // AIN0–AIN7: decimal offsets 82..97 (uint16 BE each) @@ -187,12 +163,3 @@ int _readUint16BE(Uint8List frame, int offset) { 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; -} diff --git a/lib/core/protocol/temp_table.dart b/lib/core/protocol/temp_table.dart index 0ec2c1b..d625ff1 100644 --- a/lib/core/protocol/temp_table.dart +++ b/lib/core/protocol/temp_table.dart @@ -1,5 +1,7 @@ // Shared temperature lookup table for S300 and KPro ECU protocols. -// Index = raw ECT/IAT byte value. Decoded temp = tempXlt[raw] + 40 (°C). +// Index = raw ECT/IAT byte value. Decoded temp = tempXlt[raw] - 40 (°C). +// (Subtraction, not addition: table entries span 0..190, and only +// subtracting 40 reaches the sensor's declared -40..150°C range.) const List tempXlt = [ 190, 190, 188, 186, 183, 181, 179, 177, 175, 172, 170, 168, 166, 163, 161, 159, 157, 155, 153, 151, 149, 146, 144, 142, 140, 138, 137, 136, 134, 133, 132, 130, diff --git a/lib/core/providers/sensor_provider.dart b/lib/core/providers/sensor_provider.dart index f7193c6..584ef2a 100644 --- a/lib/core/providers/sensor_provider.dart +++ b/lib/core/providers/sensor_provider.dart @@ -7,20 +7,82 @@ import '../protocol/sensor_state.dart'; import 'bt_provider.dart'; import 'settings_provider.dart'; +/// Counts of what sensorStateProvider actually did with each frame it saw, +/// exposed for the debug screen so the live pipeline can be inspected +/// directly instead of inferred from symptoms. +class DecodeStats { + final int decoded; + final int decodeErrors; + + const DecodeStats({ + this.decoded = 0, + this.decodeErrors = 0, + }); + + DecodeStats copyWith({int? decoded, int? decodeErrors}) => DecodeStats( + decoded: decoded ?? this.decoded, + decodeErrors: decodeErrors ?? this.decodeErrors, + ); +} + +final decodeStatsProvider = StateProvider((ref) => const DecodeStats()); + +// Bluetooth's round-trip jitter can make an individual request land outside +// the ECU's transient tach pulse-capture window, so a poll reports "no +// fresh count" (rpm=0) even though the engine/bench signal never actually +// changed, and jitter runs can span more than a couple of polls. Hold the +// last real RPM for this long (measured in wall-clock time, not frame count, +// since frame count at the same real gap would mean a different actual delay +// at every polling-interval setting) before accepting a run of zeros as a +// genuine stop. +const _rpmHoldDuration = Duration(milliseconds: 1000); + /// Emits a parsed [SensorState] for every live data frame received from the ECU. /// Automatically picks S300 or KPro parser based on [settingsProvider]. final sensorStateProvider = StreamProvider((ref) async* { final ecuType = ref.watch(settingsProvider).ecuType; final btNotifier = ref.watch(btProvider.notifier); - await for (final frame in btNotifier.frameStream) { - if (ecuType == EcuType.s300) { - if (isS300PlaceholderFrame(frame) && !hasS300TailRpmPayload(frame)) { - continue; + // `await for` treats any error *event* on the source stream as fatal and + // exits the loop permanently — a try/catch inside the loop body cannot + // intercept that, since it happens in the iteration mechanism itself, not + // the body. handleError() absorbs error events upstream so `await for` + // never sees them and the loop just keeps waiting for the next frame. + final safeStream = btNotifier.frameStream.handleError((Object _, StackTrace __) {}); + + double lastGoodRpm = 0; + DateTime? lastGoodRpmAt; + + await for (final frame in safeStream) { + // A single bad frame must never take down the rest of the live stream. + try { + SensorState decoded = + ecuType == EcuType.s300 ? parseS300(frame) : parseKPro(frame); + + if (decoded.rpm > 0) { + lastGoodRpm = decoded.rpm; + lastGoodRpmAt = decoded.timestamp; + } else if (lastGoodRpm > 0) { + // Anchored to the last real reading rather than refreshed on every + // held frame, so a genuine stop is accepted once this much wall-clock + // time has passed regardless of how many zero polls occurred in it. + if (decoded.timestamp.difference(lastGoodRpmAt!) < _rpmHoldDuration) { + decoded = decoded.copyWith(rpm: lastGoodRpm); + } else { + lastGoodRpm = 0; + } } - yield parseS300(frame, validateChecksum: false); - } else { - yield parseKPro(frame); + + final s = ref.read(decodeStatsProvider); + ref.read(decodeStatsProvider.notifier).state = + s.copyWith(decoded: s.decoded + 1); + yield decoded; + } catch (_) { + // Skip this one frame and keep the stream alive for the next. + final s = ref.read(decodeStatsProvider); + ref.read(decodeStatsProvider.notifier).state = + s.copyWith(decodeErrors: s.decodeErrors + 1); + continue; } } }); diff --git a/lib/ui/screens/debug/raw_frame_debug_screen.dart b/lib/ui/screens/debug/raw_frame_debug_screen.dart index 9fa6c49..3ce9cd3 100644 --- a/lib/ui/screens/debug/raw_frame_debug_screen.dart +++ b/lib/ui/screens/debug/raw_frame_debug_screen.dart @@ -16,7 +16,9 @@ class RawFrameDebugScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final bt = ref.watch(btProvider); final settings = ref.watch(settingsProvider); + final sensorAsync = ref.watch(sensorStateProvider); final state = ref.watch(latestSensorProvider); + final decodeStats = ref.watch(decodeStatsProvider); final frame = bt.lastFrame; final hex = frame == null ? '' : _hex(frame); @@ -47,17 +49,38 @@ class RawFrameDebugScreen extends ConsumerWidget { title: 'Connection', rows: [ _RowData('Protocol', settings.ecuType.name.toUpperCase()), - _RowData('Frames', bt.frameCount.toString()), - _RowData('Dropped', bt.droppedFrames.toString()), + _RowData('Frames (raw, from poller)', bt.frameCount.toString()), + _RowData('Dropped (bad checksum)', bt.droppedFrames.toString()), _RowData('Last frame bytes', frame?.length.toString() ?? 'none'), if (frame != null) _RowData( 'Checksum', - validateFrame(frame) ? 'valid' : 'bad, accepted for S300', + validateFrame(frame) ? 'valid' : 'bad', ), ], ), const SizedBox(height: 12), + _Section( + title: 'Decode Pipeline', + rows: [ + _RowData('Decoded -> UI', decodeStats.decoded.toString()), + _RowData('Skipped (decode error)', decodeStats.decodeErrors.toString()), + _RowData( + 'sensorStateProvider raw', + sensorAsync.when( + data: (s) => + 'data(rpm=${s.rpm.toStringAsFixed(0)}, age=${DateTime.now().difference(s.timestamp).inMilliseconds}ms)', + loading: () => 'loading (no frame yet)', + error: (e, st) => 'error: $e', + ), + ), + _RowData( + 'latestSensorProvider age', + '${DateTime.now().difference(state.timestamp).inMilliseconds}ms since decode', + ), + ], + ), + const SizedBox(height: 12), _Section( title: 'Current App Decode', rows: _decodedRows(state), @@ -90,8 +113,8 @@ class RawFrameDebugScreen extends ConsumerWidget { List<_RowData> _decodedRows(SensorState s) => [ _RowData('RPM', s.rpm.toStringAsFixed(0)), - _RowData('Speed mph', s.vss.toStringAsFixed(2)), - _RowData('MAP psi', s.map.toStringAsFixed(2)), + _RowData('Speed km/h', s.vss.toStringAsFixed(2)), + _RowData('MAP kPa', s.map.toStringAsFixed(2)), _RowData('TPS', s.tps.toStringAsFixed(2)), _RowData('ECT', s.ect.toStringAsFixed(2)), _RowData('IAT', s.iat.toStringAsFixed(2)), @@ -101,53 +124,46 @@ class RawFrameDebugScreen extends ConsumerWidget { ]; List<_RowData> _s300CandidateRows(Uint8List f) { - final rpmBe = _u16be(f, 2); - final rpmShiftedBe = _u16be(f, 3); - final vssBe = _u16be(f, 5); + final rpmLe = _u16le(f, 3); final vssLe = _u16le(f, 5); - final mapBe = _u16be(f, 7); final mapLe = _u16le(f, 7); return [ _RowData('Header bytes', '${_b(f, 0)} ${_b(f, 1)} ${_b(f, 2)}'), - _RowData('RPM @2 BE', rpmBe.toString()), - _RowData('RPM shifted @3 BE', rpmShiftedBe.toString()), - _RowData('VSS raw @5 BE / LE', '$vssBe / $vssLe'), - _RowData('VSS current LE mph', _s300Vss(vssLe).toStringAsFixed(2)), - _RowData('MAP raw @7 BE / LE', '$mapBe / $mapLe'), - _RowData('MAP current psi', _s300Map(mapLe).toStringAsFixed(2)), + _RowData('RPM raw @3 LE', rpmLe.toString()), + _RowData('VSS raw @5 LE', vssLe.toString()), + _RowData('VSS current km/h', _s300Vss(vssLe).toStringAsFixed(2)), + _RowData('MAP raw @7 LE', mapLe.toString()), + _RowData('MAP current kPa', _s300Map(mapLe).toStringAsFixed(2)), _RowData('TPS raw @9', f[9].toString()), _RowData('TPS current', _s300Tps(f[9]).toStringAsFixed(2)), - _RowData('INJ raw @10 LE', _u16le(f, 10).toString()), - _RowData('INJ current ms', (_u16le(f, 10) / 240.5).toStringAsFixed(2)), - _RowData('IGN raw @12', f[12].toString()), - _RowData('IGN current deg', ((f[12] - 66) / 2.0).toStringAsFixed(2)), - _RowData('O2 current V', (f[16] * 5.0 / 256.0).toStringAsFixed(2)), + _RowData('Gear raw @27 (decimal)', f[27].toString()), + _RowData('INJ raw @10 LE (offset unverified)', _u16le(f, 10).toString()), + _RowData('IGN raw @12 (offset unverified)', f[12].toString()), + _RowData('O2 current raw (offset unverified)', f[16].toString()), _RowData('ECT raw @45', f[0x2D].toString()), _RowData('IAT raw @46', f[0x2E].toString()), - _RowData('BAT raw @36', f[0x24].toString()), - _RowData('BAT current V', (f[0x24] * 26.0 / 270.0).toStringAsFixed(2)), - _RowData('PA raw @60', f[0x3C].toString()), + _RowData('BAT raw @48 (offset unverified)', f[0x30].toString()), + _RowData('PA raw @44 (offset unverified)', f[0x2C].toString()), _RowData('ERR bytes @49-52', '${_b(f, 0x31)} ${_b(f, 0x32)} ${_b(f, 0x33)} ${_b(f, 0x34)}'), ]; } - static int _u16be(Uint8List f, int o) => (f[o] << 8) | f[o + 1]; static int _u16le(Uint8List f, int o) => f[o] | (f[o + 1] << 8); static String _b(Uint8List f, int o) => f[o].toRadixString(16).padLeft(2, '0'); static double _s300Vss(int raw) { - return (raw < 893 || raw == 0xFFFF) ? 0.0 : 144256.0 / raw; + return (raw < 893 || raw == 0xFFFF) ? 0.0 : 228480.0 / raw; } static double _s300Map(int raw) { - return ((raw / 10.0) - 103.8) * 0.1450377377; + return raw / 10.0; } static double _s300Tps(int raw) { - return raw < 25 ? 0.0 : raw * 51.0 / 46.0; + return (raw * 100.0 / 255.0).clamp(0.0, 100.0); } static String _hex(Uint8List frame) { diff --git a/pubspec.lock b/pubspec.lock index 5b3c018..88994c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -244,10 +244,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" native_toolchain_c: dependency: transitive description: @@ -569,10 +569,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" typed_data: dependency: transitive description: diff --git a/test/kpro_parser_test.dart b/test/kpro_parser_test.dart index 6eb8467..090221e 100644 --- a/test/kpro_parser_test.dart +++ b/test/kpro_parser_test.dart @@ -11,7 +11,7 @@ Uint8List _buildKProFrame({ int map1 = 0, int map2 = 0, int ign = 0, - int ect = 0x80, // tempXlt[0x80]=73 → 113°C + int ect = 0x80, // tempXlt[0x80]=73 → 33°C int bat = 0, int sw1 = 0, int sw2 = 0, @@ -89,10 +89,10 @@ void main() { }); test('parses ECT using temp lookup table', () { - // ect raw = 0x80 = 128 → tempXlt[128]=73 → 113°C + // ect raw = 0x80 = 128 → tempXlt[128]=73 → 33°C final frame = _buildKProFrame(ect: 0x80); final state = parseKPro(frame); - expect(state.ect, equals((tempXlt[0x80] + 40).toDouble())); + expect(state.ect, equals((tempXlt[0x80] - 40).toDouble())); }); test('parses BAT = raw / 10', () { diff --git a/test/s300_parser_test.dart b/test/s300_parser_test.dart index 700b13c..1cfe081 100644 --- a/test/s300_parser_test.dart +++ b/test/s300_parser_test.dart @@ -12,44 +12,46 @@ Uint8List _buildS300Frame({ int tps = 0, int inj = 0, int ign = 0, - int ect = 0x80, // tempXlt[0x80]=73 → 73+40=113°C + int ect = 0x80, // tempXlt[0x80]=73 → 73-40=33°C int bat = 0, int sw1 = 0, int sw2 = 0, int sw3 = 0, int krtrd = 0, + int gear = 0, }) { final frame = Uint8List(128); // Header frame[0] = 0x1B; frame[1] = 0x00; - // RPM: bytes 2..3, raw stream is display RPM + 256 - final rpmRaw = rpm + 256; - frame[2] = (rpmRaw >> 8) & 0xFF; - frame[3] = rpmRaw & 0xFF; - // VSS: bytes 5..6 little-endian + // RPM: bytes +3..4, little-endian, raw + frame[3] = rpm & 0xFF; + frame[4] = (rpm >> 8) & 0xFF; + // VSS: bytes +5..6, little-endian frame[5] = vss & 0xFF; frame[6] = (vss >> 8) & 0xFF; - // MAP: bytes 7..8 little-endian + // MAP: bytes +7..8, little-endian frame[7] = mapRaw & 0xFF; frame[8] = (mapRaw >> 8) & 0xFF; - // TPS: byte 9 + // TPS: byte +9 frame[9] = tps; - // INJ: bytes 10..11 little-endian - frame[10] = inj & 0xFF; - frame[11] = (inj >> 8) & 0xFF; - // IGN: byte 12 + // INJ: bytes +10..11, BE + frame[10] = (inj >> 8) & 0xFF; + frame[11] = inj & 0xFF; + // IGN: byte +12 frame[12] = ign; - // KRtrd: byte 13 + // KRtrd: byte +13 frame[13] = krtrd; // SW bitmaps frame[0x11] = sw1; frame[0x12] = sw2; frame[0x13] = sw3; - // ECT: byte 0x2D + // Gear: decimal offset 27 + frame[27] = gear; + // ECT: byte +2D frame[0x2D] = ect; - // BAT: byte 0x24 - frame[0x24] = bat; + // BAT: byte +30 + frame[0x30] = bat; // Stamp NEG8 checksum at byte 127 int neg8 = 0; @@ -76,37 +78,49 @@ void main() { }); test('parses VSS correctly when raw >= 893', () { - // vss = 144256 / 1000 = 144.256 mph + // vss = 228480 / 1000 = 228.48 km/h final frame = _buildS300Frame(vss: 1000); final state = parseS300(frame); - expect(state.vss, closeTo(144.26, 0.01)); + expect(state.vss, closeTo(228.48, 0.01)); }); - test('parses MAP as SManager-style psi', () { - // mapRaw = 1148 -> 114.8 kPa absolute -> about 1.6 psi displayed + test('parses MAP as raw/10 kPa', () { + // mapRaw = 1148 -> 114.8 kPa final frame = _buildS300Frame(mapRaw: 1148); final state = parseS300(frame); - expect(state.map, closeTo(1.6, 0.01)); + expect(state.map, closeTo(114.8, 0.01)); }); - test('parses TPS = 0 when raw < 25', () { - final frame = _buildS300Frame(tps: 10); + test('parses TPS = 0 when raw = 0', () { + final frame = _buildS300Frame(tps: 0); final state = parseS300(frame); expect(state.tps, equals(0.0)); }); - test('parses TPS correctly when raw >= 25', () { - // tps raw=134 -> approximately 51% - final frame = _buildS300Frame(tps: 134); + test('parses TPS = 100 when raw = 255', () { + final frame = _buildS300Frame(tps: 255); final state = parseS300(frame); - expect(state.tps, closeTo(50.93, 0.01)); + expect(state.tps, equals(100.0)); + }); + + test('parses TPS correctly when raw >= 25', () { + // tps raw=127 -> 127*100/255 ≈ 49.8% (confirmed against live capture) + final frame = _buildS300Frame(tps: 127); + final state = parseS300(frame); + expect(state.tps, closeTo(49.8, 0.1)); + }); + + test('parses Gear from decimal offset 27', () { + final frame = _buildS300Frame(gear: 5); + final state = parseS300(frame); + expect(state.gear, equals(5)); }); test('parses ECT using temp lookup table', () { - // ect raw = 0x80 = 128 → tempXlt[128]=73 → 73+40=113°C + // ect raw = 0x80 = 128 → tempXlt[128]=73 → 73-40=33°C final frame = _buildS300Frame(ect: 0x80); final state = parseS300(frame); - expect(state.ect, equals((tempXlt[0x80] + 40).toDouble())); + expect(state.ect, equals((tempXlt[0x80] - 40).toDouble())); }); test('parses BAT = raw * 26 / 270', () { @@ -117,10 +131,10 @@ void main() { }); test('IGN decode: (raw + 120) / 2', () { - // ign raw = 164 -> 49.0° + // ign raw = 164 -> (164 + 120) / 2 = 142.0 final frame = _buildS300Frame(ign: 164); final state = parseS300(frame); - expect(state.ign, equals(49.0)); + expect(state.ign, equals(142.0)); }); test('MIL flag set from SW2 bit5', () { @@ -161,36 +175,13 @@ void main() { expect(state.rpm, equals(1000.0)); }); - test('detects known S300 prefix signature', () { - final frame = Uint8List(128); - final signature = [ - 0x1B, - 0x00, - 0x14, - 0x00, - 0x00, - 0xFF, - 0xFF, - 0xF2, - 0x03, - 0x18, - 0x00, - 0x00, - 0x10, - 0x00, - 0x00, - 0x00, - 0xC4, - ]; - for (var i = 0; i < signature.length; i++) { - frame[i] = signature[i]; - } - frame[127] = calculateNeg8(frame.sublist(0, 127)); - - expect(isS300PlaceholderFrame(frame), isTrue); - }); - - test('uses calibrated tail payload RPM at offset 82', () { + test('decodes RPM=0 for a frame matching the old "keep-alive" byte prefix', () { + // A fixed 17-byte prefix here was previously treated as a keep-alive + // signature to skip outright. Live testing showed it over-matched + // badly: with RPM actively changing, nearly every frame was being + // classified as a keep-alive and discarded, freezing the dashboard. + // Every checksum-valid frame is decoded normally now regardless of + // its leading bytes — see sensor_provider.dart. final frame = Uint8List(128); final prefix = [ 0x1B, @@ -214,75 +205,10 @@ void main() { for (var i = 0; i < prefix.length; i++) { frame[i] = prefix[i]; } - frame[82] = 0x32; - frame[83] = 0x0C; frame[127] = calculateNeg8(frame.sublist(0, 127)); final state = parseS300(frame); - expect(state.rpm, equals(2033.0)); - }); - - test('uses calibrated tail payload RPM at offset 86', () { - final frame = Uint8List(128); - final prefix = [ - 0x1B, - 0x00, - 0x14, - 0x00, - 0x00, - 0xFF, - 0xFF, - 0xF2, - 0x03, - 0x18, - 0x00, - 0x00, - 0x10, - 0x00, - 0x00, - 0x00, - 0xC4, - ]; - for (var i = 0; i < prefix.length; i++) { - frame[i] = prefix[i]; - } - frame[86] = 0x31; - frame[87] = 0x0C; - frame[127] = calculateNeg8(frame.sublist(0, 127)); - - expect(hasS300TailRpmPayload(frame), isTrue); - final state = parseS300(frame); - expect(state.rpm, equals(2032.0)); - }); - - test('detects empty S300 Bluetooth prefix frames', () { - final frame = Uint8List(128); - final prefix = [ - 0x1B, - 0x00, - 0x14, - 0x00, - 0x00, - 0xFF, - 0xFF, - 0xF2, - 0x03, - 0x18, - 0x00, - 0x00, - 0x10, - 0x00, - 0x00, - 0x00, - 0xC4, - ]; - for (var i = 0; i < prefix.length; i++) { - frame[i] = prefix[i]; - } - frame[127] = calculateNeg8(frame.sublist(0, 127)); - - expect(isS300PlaceholderFrame(frame), isTrue); - expect(hasS300TailRpmPayload(frame), isFalse); + expect(state.rpm, equals(0.0)); }); }); } diff --git a/test/temp_table_test.dart b/test/temp_table_test.dart index 587b777..973a685 100644 --- a/test/temp_table_test.dart +++ b/test/temp_table_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hvbt_dash/core/protocol/temp_table.dart'; -double decodeTemp(int raw) => (tempXlt[raw] + 40).toDouble(); +double decodeTemp(int raw) => (tempXlt[raw] - 40).toDouble(); void main() { group('Temperature lookup table', () { @@ -9,22 +9,22 @@ void main() { expect(tempXlt.length, equals(256)); }); - test('decodeTemp(0x00) = tempXlt[0] + 40 = 190 + 40 = 230°C', () { - expect(decodeTemp(0x00), equals(230.0)); + test('decodeTemp(0x00) = tempXlt[0] - 40 = 190 - 40 = 150°C', () { + expect(decodeTemp(0x00), equals(150.0)); }); - test('decodeTemp(0xA0) = tempXlt[160] + 40', () { + test('decodeTemp(0xA0) = tempXlt[160] - 40', () { // 0xA0 = 160 decimal; tempXlt[160] = 60 - expect(decodeTemp(0xA0), equals(60 + 40.0)); + expect(decodeTemp(0xA0), equals(60 - 40.0)); }); - test('decodeTemp(0xFF) = tempXlt[255] + 40 = 0 + 40 = 40°C', () { - expect(decodeTemp(0xFF), equals(40.0)); + test('decodeTemp(0xFF) = tempXlt[255] - 40 = 0 - 40 = -40°C', () { + expect(decodeTemp(0xFF), equals(-40.0)); }); - test('decodeTemp(0x80) = tempXlt[128] + 40', () { + test('decodeTemp(0x80) = tempXlt[128] - 40', () { // 0x80 = 128; tempXlt[128] = 73 - expect(decodeTemp(0x80), equals(73 + 40.0)); + expect(decodeTemp(0x80), equals(73 - 40.0)); }); test('all table values are non-negative', () { @@ -32,5 +32,11 @@ void main() { expect(v, greaterThanOrEqualTo(0)); } }); + + test('decoded range matches the declared -40..150°C sensor range', () { + final decoded = tempXlt.map((v) => v - 40).toList(); + expect(decoded.reduce((a, b) => a > b ? a : b), equals(150)); + expect(decoded.reduce((a, b) => a < b ? a : b), equals(-40)); + }); }); }