import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../datalog/datalog_db.dart'; import '../datalog/datalog_player.dart'; import '../datalog/datalog_recorder.dart'; import '../models/datalog_session.dart'; import '../bluetooth/bt_poller.dart'; import 'bt_provider.dart'; import 'sensor_history_provider.dart'; import 'settings_provider.dart'; // ─── Singletons ─────────────────────────────────────────────────────────────── final datalogDbProvider = Provider((ref) { final db = DatalogDb(); ref.onDispose(db.close); return db; }); final datalogRecorderProvider = Provider((ref) { final recorder = DatalogRecorder(ref.watch(datalogDbProvider)); ref.onDispose(recorder.dispose); return recorder; }); final datalogPlayerProvider = Provider((ref) { final player = DatalogPlayer(ref.watch(datalogDbProvider)); ref.onDispose(player.dispose); return player; }); // ─── Recording state ────────────────────────────────────────────────────────── enum RecordingStatus { idle, recording } class RecordingState { final RecordingStatus status; final int frameCount; final int? sessionId; const RecordingState({ this.status = RecordingStatus.idle, this.frameCount = 0, this.sessionId, }); bool get isRecording => status == RecordingStatus.recording; RecordingState copyWith({ RecordingStatus? status, int? frameCount, int? sessionId, }) => RecordingState( status: status ?? this.status, frameCount: frameCount ?? this.frameCount, sessionId: sessionId ?? this.sessionId, ); } class RecordingNotifier extends StateNotifier { final Ref _ref; RecordingNotifier(this._ref) : super(const RecordingState()); Future startRecording() async { if (state.isRecording) return; final recorder = _ref.read(datalogRecorderProvider); final btNotifier = _ref.read(btProvider.notifier); final ecuType = _ref.read(settingsProvider).ecuType.name; await recorder.start(btNotifier.frameStream, ecuType); state = RecordingState( status: RecordingStatus.recording, sessionId: recorder.sessionId, ); } Future stopRecording() async { if (!state.isRecording) return null; final recorder = _ref.read(datalogRecorderProvider); final session = await recorder.stop(); state = const RecordingState(); // Refresh session list _ref.invalidate(sessionListProvider); return session; } /// Called periodically from UI to update live frame count display. void syncFrameCount() { if (!state.isRecording) return; final count = _ref.read(datalogRecorderProvider).frameCount; if (count != state.frameCount) { state = state.copyWith(frameCount: count); } } } final recordingProvider = StateNotifierProvider( (ref) => RecordingNotifier(ref), ); // ─── Session list ───────────────────────────────────────────────────────────── final sessionListProvider = FutureProvider>((ref) { return ref.watch(datalogDbProvider).getSessions(); }); // ─── Playback state ─────────────────────────────────────────────────────────── class PlaybackState { final bool isActive; final int? sessionId; /// ECU type the session was recorded with; frames are parsed with it. final EcuType ecuType; final int position; final int total; final double speed; final PlaybackStatus status; const PlaybackState({ this.isActive = false, this.sessionId, this.ecuType = EcuType.s300, this.position = 0, this.total = 0, this.speed = 1.0, this.status = PlaybackStatus.idle, }); double get progress => total == 0 ? 0 : position / total; PlaybackState copyWith({ bool? isActive, int? sessionId, int? position, int? total, double? speed, PlaybackStatus? status, }) => PlaybackState( isActive: isActive ?? this.isActive, sessionId: sessionId ?? this.sessionId, ecuType: ecuType, position: position ?? this.position, total: total ?? this.total, speed: speed ?? this.speed, status: status ?? this.status, ); } class PlaybackNotifier extends StateNotifier { final Ref _ref; PlaybackNotifier(this._ref) : super(const PlaybackState()); /// Loads a recorded session; while active, the dashboard, sensor list, /// graph and DTC screen show the recording instead of live data. Future loadSession(int sessionId, String ecuType) async { final player = _ref.read(datalogPlayerProvider); await player.load(sessionId); _ref.read(sensorHistoryProvider.notifier).clear(); player.onProgress = (pos, total) { state = state.copyWith( position: pos, total: total, status: player.status, ); }; state = PlaybackState( isActive: true, sessionId: sessionId, ecuType: ecuType == EcuType.kpro.name ? EcuType.kpro : EcuType.s300, total: player.totalFrames, speed: player.speed, status: PlaybackStatus.idle, ); } void play() { _ref.read(datalogPlayerProvider).play(); state = state.copyWith(status: PlaybackStatus.playing); } void pause() { _ref.read(datalogPlayerProvider).pause(); state = state.copyWith(status: PlaybackStatus.paused); } Future stop() async { await _ref.read(datalogPlayerProvider).stop(); state = const PlaybackState(); _ref.read(sensorHistoryProvider.notifier).clear(); } void seek(int index) { _ref.read(datalogPlayerProvider).seek(index); state = state.copyWith(position: index); } void setSpeed(double speed) { _ref.read(datalogPlayerProvider).speed = speed; state = state.copyWith(speed: speed); } } final playbackProvider = StateNotifierProvider( (ref) => PlaybackNotifier(ref), ); /// ECU type the displays should decode with: the recorded session's while a /// datalog is playing back, otherwise the live setting. final effectiveEcuTypeProvider = Provider((ref) { final playback = ref.watch(playbackProvider); if (playback.isActive) return playback.ecuType; return ref.watch(settingsProvider.select((s) => s.ecuType)); });