96 lines
3.8 KiB
Dart
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();
|
|
});
|