hondavert-dev/lib/core/providers/sensor_provider.dart
2026-08-08 13:18:02 -04:00

96 lines
3.8 KiB
Dart

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<DecodeStats>((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<SensorState>((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<SensorState>((ref) {
return ref.watch(sensorStateProvider).whenData((s) => s).value ??
SensorState.zero();
});