diff --git a/lib/core/protocol/s300_parser.dart b/lib/core/protocol/s300_parser.dart index b3593cd..9d1615b 100644 --- a/lib/core/protocol/s300_parser.dart +++ b/lib/core/protocol/s300_parser.dart @@ -28,6 +28,10 @@ bool isS300PlaceholderFrame(Uint8List frame) { 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. @@ -39,13 +43,12 @@ 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 at 82..83, + // 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 bool hasTailPayload = _hasS300TailPayload(frame); - final int rpmRaw = - hasTailPayload ? _readUint16LE(frame, 82) : _readUint16BE(frame, 2); - final double rpm = (hasTailPayload ? rpmRaw - 1089 : rpmRaw - 256) + 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(); @@ -185,8 +188,11 @@ int _readUint16LE(Uint8List frame, int offset) { return frame[offset] | (frame[offset + 1] << 8); } -bool _hasS300TailPayload(Uint8List frame) { - if (!isS300PlaceholderFrame(frame)) return false; - final rpm = _readUint16LE(frame, 82); - return rpm > 300 && rpm < 12000; +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/providers/sensor_provider.dart b/lib/core/providers/sensor_provider.dart index 8453a39..f7193c6 100644 --- a/lib/core/providers/sensor_provider.dart +++ b/lib/core/providers/sensor_provider.dart @@ -15,6 +15,9 @@ final sensorStateProvider = StreamProvider((ref) async* { await for (final frame in btNotifier.frameStream) { if (ecuType == EcuType.s300) { + if (isS300PlaceholderFrame(frame) && !hasS300TailRpmPayload(frame)) { + continue; + } yield parseS300(frame, validateChecksum: false); } else { yield parseKPro(frame); diff --git a/test/s300_parser_test.dart b/test/s300_parser_test.dart index 4ac6682..700b13c 100644 --- a/test/s300_parser_test.dart +++ b/test/s300_parser_test.dart @@ -190,8 +190,7 @@ void main() { expect(isS300PlaceholderFrame(frame), isTrue); }); - test('uses calibrated tail payload RPM for S300 Bluetooth prefix frames', - () { + test('uses calibrated tail payload RPM at offset 82', () { final frame = Uint8List(128); final prefix = [ 0x1B, @@ -222,5 +221,68 @@ void main() { 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); + }); }); }