hondavert-dev/lib/core/providers/datalog_provider.dart
HVBT Dev 332dd6209e v1.3.0 — Phase 3: SQLite datalog (DB + recorder + player + providers)
- datalog_session.dart: DatalogSession model with toMap/fromMap, duration helpers
- datalog_db.dart: SQLite schema (sessions + frames tables), batch insertFrames,
  getSessions, getFrames, deleteSession, closeSession
- datalog_recorder.dart: listens to raw frame stream, buffers PendingFrames,
  flushes to DB every 500ms via Timer, finalises session on stop()
- datalog_player.dart: loads StoredFrames from DB, replays at original timing
  adjusted by speed multiplier (0.25x–8x), play/pause/stop/seek, onProgress cb
- datalog_provider.dart: datalogDbProvider, datalogRecorderProvider,
  datalogPlayerProvider singletons; RecordingNotifier (idle/recording state);
  sessionListProvider FutureProvider; PlaybackNotifier with full playback state
- pubspec.yaml: added path ^1.9.0 dependency
2026-04-13 20:42:26 +05:30

207 lines
5.8 KiB
Dart

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 'bt_provider.dart';
import 'settings_provider.dart';
// ─── Singletons ───────────────────────────────────────────────────────────────
final datalogDbProvider = Provider<DatalogDb>((ref) {
final db = DatalogDb();
ref.onDispose(db.close);
return db;
});
final datalogRecorderProvider = Provider<DatalogRecorder>((ref) {
final recorder = DatalogRecorder(ref.watch(datalogDbProvider));
ref.onDispose(recorder.dispose);
return recorder;
});
final datalogPlayerProvider = Provider<DatalogPlayer>((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<RecordingState> {
final Ref _ref;
RecordingNotifier(this._ref) : super(const RecordingState());
Future<void> 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<DatalogSession?> 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<RecordingNotifier, RecordingState>(
(ref) => RecordingNotifier(ref),
);
// ─── Session list ─────────────────────────────────────────────────────────────
final sessionListProvider = FutureProvider<List<DatalogSession>>((ref) {
return ref.watch(datalogDbProvider).getSessions();
});
// ─── Playback state ───────────────────────────────────────────────────────────
class PlaybackState {
final bool isActive;
final int? sessionId;
final int position;
final int total;
final double speed;
final PlaybackStatus status;
const PlaybackState({
this.isActive = false,
this.sessionId,
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,
position: position ?? this.position,
total: total ?? this.total,
speed: speed ?? this.speed,
status: status ?? this.status,
);
}
class PlaybackNotifier extends StateNotifier<PlaybackState> {
final Ref _ref;
PlaybackNotifier(this._ref) : super(const PlaybackState());
Future<void> loadSession(int sessionId) async {
final player = _ref.read(datalogPlayerProvider);
await player.load(sessionId);
player.onProgress = (pos, total) {
state = state.copyWith(
position: pos,
total: total,
status: player.status,
);
};
state = PlaybackState(
isActive: true,
sessionId: sessionId,
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<void> stop() async {
await _ref.read(datalogPlayerProvider).stop();
state = const PlaybackState();
}
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<PlaybackNotifier, PlaybackState>(
(ref) => PlaybackNotifier(ref),
);