- 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
141 lines
4.5 KiB
Dart
141 lines
4.5 KiB
Dart
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});
|
|
}
|