hondavert-dev/lib/core/datalog/datalog_db.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

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});
}