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
This commit is contained in:
parent
6a80d8fc1f
commit
332dd6209e
140
lib/core/datalog/datalog_db.dart
Normal file
140
lib/core/datalog/datalog_db.dart
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
|
import '../models/datalog_session.dart';
|
||||||
|
|
||||||
|
/// SQLite database wrapper for sessions and raw ECU frames.
|
||||||
|
class DatalogDb {
|
||||||
|
static const _dbName = 'hvbt_datalog.db';
|
||||||
|
static const _version = 1;
|
||||||
|
|
||||||
|
Database? _db;
|
||||||
|
|
||||||
|
Future<Database> get _database async {
|
||||||
|
_db ??= await _open();
|
||||||
|
return _db!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Database> _open() async {
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
final path = p.join(dir.path, _dbName);
|
||||||
|
return openDatabase(
|
||||||
|
path,
|
||||||
|
version: _version,
|
||||||
|
onCreate: (db, _) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ecu_type TEXT NOT NULL,
|
||||||
|
start_time INTEGER NOT NULL,
|
||||||
|
end_time INTEGER,
|
||||||
|
frame_count INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE frames (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_id INTEGER NOT NULL,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
data BLOB NOT NULL,
|
||||||
|
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_frames_session ON frames(session_id)');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sessions ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Inserts a new session row. Returns the assigned id.
|
||||||
|
Future<int> insertSession(DatalogSession session) async {
|
||||||
|
final db = await _database;
|
||||||
|
return db.insert('sessions', session.toMap()..remove('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates end_time and frame_count for an existing session.
|
||||||
|
Future<void> closeSession(int sessionId, DateTime endTime, int frameCount) async {
|
||||||
|
final db = await _database;
|
||||||
|
await db.update(
|
||||||
|
'sessions',
|
||||||
|
{
|
||||||
|
'end_time': endTime.millisecondsSinceEpoch,
|
||||||
|
'frame_count': frameCount,
|
||||||
|
},
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [sessionId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all sessions ordered newest first.
|
||||||
|
Future<List<DatalogSession>> getSessions() async {
|
||||||
|
final db = await _database;
|
||||||
|
final rows = await db.query('sessions', orderBy: 'start_time DESC');
|
||||||
|
return rows.map(DatalogSession.fromMap).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a session and all its frames (CASCADE).
|
||||||
|
Future<void> deleteSession(int sessionId) async {
|
||||||
|
final db = await _database;
|
||||||
|
await db.delete('sessions', where: 'id = ?', whereArgs: [sessionId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Frames ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Batch-inserts multiple frames in a single transaction.
|
||||||
|
Future<void> insertFrames(
|
||||||
|
int sessionId, List<PendingFrame> frames) async {
|
||||||
|
if (frames.isEmpty) return;
|
||||||
|
final db = await _database;
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
final batch = txn.batch();
|
||||||
|
for (final f in frames) {
|
||||||
|
batch.insert('frames', {
|
||||||
|
'session_id': sessionId,
|
||||||
|
'timestamp': f.timestamp.millisecondsSinceEpoch,
|
||||||
|
'data': f.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all raw frames for a session, ordered by timestamp.
|
||||||
|
Future<List<StoredFrame>> getFrames(int sessionId) async {
|
||||||
|
final db = await _database;
|
||||||
|
final rows = await db.query(
|
||||||
|
'frames',
|
||||||
|
where: 'session_id = ?',
|
||||||
|
whereArgs: [sessionId],
|
||||||
|
orderBy: 'timestamp ASC',
|
||||||
|
);
|
||||||
|
return rows
|
||||||
|
.map((r) => StoredFrame(
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
r['timestamp'] as int),
|
||||||
|
data: r['data'] as Uint8List,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> close() async => _db?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DTOs ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class PendingFrame {
|
||||||
|
final DateTime timestamp;
|
||||||
|
final Uint8List data;
|
||||||
|
const PendingFrame(this.timestamp, this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoredFrame {
|
||||||
|
final DateTime timestamp;
|
||||||
|
final Uint8List data;
|
||||||
|
const StoredFrame({required this.timestamp, required this.data});
|
||||||
|
}
|
||||||
119
lib/core/datalog/datalog_player.dart
Normal file
119
lib/core/datalog/datalog_player.dart
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'datalog_db.dart';
|
||||||
|
|
||||||
|
enum PlaybackStatus { idle, playing, paused }
|
||||||
|
|
||||||
|
/// Loads stored frames from SQLite and replays them through a stream
|
||||||
|
/// at configurable speed. Consumers watch [frameStream] exactly like
|
||||||
|
/// the live BT stream — the parser layer is unaware of the difference.
|
||||||
|
class DatalogPlayer {
|
||||||
|
final DatalogDb _db;
|
||||||
|
|
||||||
|
List<StoredFrame> _frames = [];
|
||||||
|
int _position = 0; // index into _frames
|
||||||
|
double _speed = 1.0; // 1.0 = real-time, 2.0 = 2× faster
|
||||||
|
|
||||||
|
PlaybackStatus _status = PlaybackStatus.idle;
|
||||||
|
PlaybackStatus get status => _status;
|
||||||
|
int get position => _position;
|
||||||
|
int get totalFrames => _frames.length;
|
||||||
|
double get speed => _speed;
|
||||||
|
set speed(double v) => _speed = v.clamp(0.25, 8.0);
|
||||||
|
|
||||||
|
final StreamController<Uint8List> _controller =
|
||||||
|
StreamController<Uint8List>.broadcast();
|
||||||
|
|
||||||
|
Stream<Uint8List> get frameStream => _controller.stream;
|
||||||
|
|
||||||
|
/// Callback fired on each tick — lets UI update a progress indicator.
|
||||||
|
void Function(int position, int total)? onProgress;
|
||||||
|
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
DatalogPlayer(this._db);
|
||||||
|
|
||||||
|
/// Load all frames for [sessionId] into memory.
|
||||||
|
Future<void> load(int sessionId) async {
|
||||||
|
await stop();
|
||||||
|
_frames = await _db.getFrames(sessionId);
|
||||||
|
_position = 0;
|
||||||
|
_status = PlaybackStatus.idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start or resume playback.
|
||||||
|
void play() {
|
||||||
|
if (_status == PlaybackStatus.playing) return;
|
||||||
|
if (_frames.isEmpty) return;
|
||||||
|
_status = PlaybackStatus.playing;
|
||||||
|
_scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
if (_status == PlaybackStatus.playing) {
|
||||||
|
_status = PlaybackStatus.paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
_position = 0;
|
||||||
|
_status = PlaybackStatus.idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jump to a specific frame index.
|
||||||
|
void seek(int index) {
|
||||||
|
_position = index.clamp(0, _frames.length - 1);
|
||||||
|
onProgress?.call(_position, _frames.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleNext() {
|
||||||
|
if (_position >= _frames.length) {
|
||||||
|
_status = PlaybackStatus.idle;
|
||||||
|
onProgress?.call(_frames.length, _frames.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate delay to next frame using original timestamps
|
||||||
|
Duration delay = const Duration(milliseconds: 100); // default 10 Hz
|
||||||
|
if (_position + 1 < _frames.length) {
|
||||||
|
final gap = _frames[_position + 1].timestamp
|
||||||
|
.difference(_frames[_position].timestamp);
|
||||||
|
delay = Duration(
|
||||||
|
microseconds: (gap.inMicroseconds / _speed).round(),
|
||||||
|
);
|
||||||
|
// Clamp to 10ms–2s to handle gaps from pauses/reconnects
|
||||||
|
delay = delay.clamp(
|
||||||
|
const Duration(milliseconds: 10),
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_timer = Timer(delay, () {
|
||||||
|
if (_status != PlaybackStatus.playing) return;
|
||||||
|
if (_position < _frames.length) {
|
||||||
|
_controller.add(_frames[_position].data);
|
||||||
|
onProgress?.call(_position, _frames.length);
|
||||||
|
_position++;
|
||||||
|
}
|
||||||
|
_scheduleNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Duration {
|
||||||
|
Duration clamp(Duration min, Duration max) {
|
||||||
|
if (this < min) return min;
|
||||||
|
if (this > max) return max;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/core/datalog/datalog_recorder.dart
Normal file
94
lib/core/datalog/datalog_recorder.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'datalog_db.dart';
|
||||||
|
import '../models/datalog_session.dart';
|
||||||
|
|
||||||
|
/// Listens to a raw frame stream and batches writes to SQLite every 500ms.
|
||||||
|
class DatalogRecorder {
|
||||||
|
final DatalogDb _db;
|
||||||
|
|
||||||
|
int? _sessionId;
|
||||||
|
String? _ecuType;
|
||||||
|
DateTime? _startTime;
|
||||||
|
int _frameCount = 0;
|
||||||
|
|
||||||
|
final List<PendingFrame> _batch = [];
|
||||||
|
Timer? _flushTimer;
|
||||||
|
StreamSubscription<Uint8List>? _frameSub;
|
||||||
|
|
||||||
|
bool get isRecording => _sessionId != null;
|
||||||
|
int get frameCount => _frameCount;
|
||||||
|
int? get sessionId => _sessionId;
|
||||||
|
|
||||||
|
DatalogRecorder(this._db);
|
||||||
|
|
||||||
|
/// Start a new recording session, listening to [frameStream].
|
||||||
|
Future<void> start(Stream<Uint8List> frameStream, String ecuType) async {
|
||||||
|
if (isRecording) await stop();
|
||||||
|
|
||||||
|
_ecuType = ecuType;
|
||||||
|
_startTime = DateTime.now();
|
||||||
|
_frameCount = 0;
|
||||||
|
_batch.clear();
|
||||||
|
|
||||||
|
final session = DatalogSession(
|
||||||
|
ecuType: ecuType,
|
||||||
|
startTime: _startTime!,
|
||||||
|
);
|
||||||
|
_sessionId = await _db.insertSession(session);
|
||||||
|
|
||||||
|
// Collect frames into batch buffer
|
||||||
|
_frameSub = frameStream.listen((Uint8List frame) {
|
||||||
|
_batch.add(PendingFrame(DateTime.now(), frame));
|
||||||
|
_frameCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flush to DB every 500ms
|
||||||
|
_flushTimer = Timer.periodic(const Duration(milliseconds: 500), (_) {
|
||||||
|
_flush();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop recording and finalise the session row.
|
||||||
|
Future<DatalogSession?> stop() async {
|
||||||
|
if (!isRecording) return null;
|
||||||
|
|
||||||
|
_flushTimer?.cancel();
|
||||||
|
_flushTimer = null;
|
||||||
|
await _frameSub?.cancel();
|
||||||
|
_frameSub = null;
|
||||||
|
|
||||||
|
await _flush(); // write any remaining buffered frames
|
||||||
|
|
||||||
|
final endTime = DateTime.now();
|
||||||
|
await _db.closeSession(_sessionId!, endTime, _frameCount);
|
||||||
|
|
||||||
|
final finished = DatalogSession(
|
||||||
|
id: _sessionId,
|
||||||
|
ecuType: _ecuType!,
|
||||||
|
startTime: _startTime!,
|
||||||
|
endTime: endTime,
|
||||||
|
frameCount: _frameCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
_sessionId = null;
|
||||||
|
_ecuType = null;
|
||||||
|
_startTime = null;
|
||||||
|
_frameCount = 0;
|
||||||
|
|
||||||
|
return finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _flush() async {
|
||||||
|
if (_batch.isEmpty || _sessionId == null) return;
|
||||||
|
final toWrite = List<PendingFrame>.from(_batch);
|
||||||
|
_batch.clear();
|
||||||
|
await _db.insertFrames(_sessionId!, toWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_flushTimer?.cancel();
|
||||||
|
_frameSub?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
62
lib/core/models/datalog_session.dart
Normal file
62
lib/core/models/datalog_session.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/// Metadata for a single recorded ECU datalog session.
|
||||||
|
class DatalogSession {
|
||||||
|
final int? id;
|
||||||
|
final String ecuType;
|
||||||
|
final DateTime startTime;
|
||||||
|
final DateTime? endTime;
|
||||||
|
final int frameCount;
|
||||||
|
|
||||||
|
const DatalogSession({
|
||||||
|
this.id,
|
||||||
|
required this.ecuType,
|
||||||
|
required this.startTime,
|
||||||
|
this.endTime,
|
||||||
|
this.frameCount = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
Duration get duration {
|
||||||
|
final end = endTime ?? DateTime.now();
|
||||||
|
return end.difference(startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get durationLabel {
|
||||||
|
final d = duration;
|
||||||
|
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||||
|
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||||
|
return '$m:$s';
|
||||||
|
}
|
||||||
|
|
||||||
|
DatalogSession copyWith({
|
||||||
|
int? id,
|
||||||
|
String? ecuType,
|
||||||
|
DateTime? startTime,
|
||||||
|
DateTime? endTime,
|
||||||
|
int? frameCount,
|
||||||
|
}) =>
|
||||||
|
DatalogSession(
|
||||||
|
id: id ?? this.id,
|
||||||
|
ecuType: ecuType ?? this.ecuType,
|
||||||
|
startTime: startTime ?? this.startTime,
|
||||||
|
endTime: endTime ?? this.endTime,
|
||||||
|
frameCount: frameCount ?? this.frameCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'id': id,
|
||||||
|
'ecu_type': ecuType,
|
||||||
|
'start_time': startTime.millisecondsSinceEpoch,
|
||||||
|
'end_time': endTime?.millisecondsSinceEpoch,
|
||||||
|
'frame_count': frameCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory DatalogSession.fromMap(Map<String, dynamic> m) => DatalogSession(
|
||||||
|
id: m['id'] as int?,
|
||||||
|
ecuType: m['ecu_type'] as String,
|
||||||
|
startTime:
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(m['start_time'] as int),
|
||||||
|
endTime: m['end_time'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(m['end_time'] as int),
|
||||||
|
frameCount: m['frame_count'] as int? ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
206
lib/core/providers/datalog_provider.dart
Normal file
206
lib/core/providers/datalog_provider.dart
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
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),
|
||||||
|
);
|
||||||
@ -273,7 +273,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
|||||||
@ -3,7 +3,7 @@ description: HV BT Automotive ECU Dashboard
|
|||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.2.0+3
|
version: 1.3.0+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@ -18,6 +18,7 @@ dependencies:
|
|||||||
shared_preferences: ^2.2.0
|
shared_preferences: ^2.2.0
|
||||||
path_provider: ^2.1.0
|
path_provider: ^2.1.0
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
path: ^1.9.0
|
||||||
permission_handler: ^11.0.0
|
permission_handler: ^11.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user