import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../bluetooth/bt_poller.dart'; import '../protocol/kpro_parser.dart'; import '../protocol/s300_parser.dart'; 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` 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; } } 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; } } }); /// Last successfully parsed sensor state (never null after first frame). /// Falls back to SensorState.zero() before any data arrives. final latestSensorProvider = Provider((ref) { return ref.watch(sensorStateProvider).whenData((s) => s).value ?? SensorState.zero(); });