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 get _database async { _db ??= await _open(); return _db!; } Future _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 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 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> 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 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 insertFrames( int sessionId, List 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> 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 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}); }