mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add packet trace storage flow
This commit is contained in:
146
lib/services/packet_capture_storage_service.dart
Normal file
146
lib/services/packet_capture_storage_service.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../models/ble_packet_log.dart';
|
||||
|
||||
class StoredPacketCapture {
|
||||
final DateTime timestamp;
|
||||
final String direction;
|
||||
final int? responseCode;
|
||||
final String? description;
|
||||
final String rawBase64;
|
||||
final int rawSize;
|
||||
final double? snrDb;
|
||||
final int? rssiDbm;
|
||||
|
||||
const StoredPacketCapture({
|
||||
required this.timestamp,
|
||||
required this.direction,
|
||||
required this.responseCode,
|
||||
required this.description,
|
||||
required this.rawBase64,
|
||||
required this.rawSize,
|
||||
required this.snrDb,
|
||||
required this.rssiDbm,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'ts': timestamp.millisecondsSinceEpoch,
|
||||
'dir': direction,
|
||||
'code': responseCode,
|
||||
'desc': description,
|
||||
'raw': rawBase64,
|
||||
'size': rawSize,
|
||||
'snr': snrDb,
|
||||
'rssi': rssiDbm,
|
||||
};
|
||||
}
|
||||
|
||||
static StoredPacketCapture fromJson(Map<String, dynamic> json) {
|
||||
return StoredPacketCapture(
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch((json['ts'] as num).toInt()),
|
||||
direction: (json['dir'] as String?) ?? 'rx',
|
||||
responseCode: (json['code'] as num?)?.toInt(),
|
||||
description: json['desc'] as String?,
|
||||
rawBase64: (json['raw'] as String?) ?? '',
|
||||
rawSize: (json['size'] as num?)?.toInt() ?? 0,
|
||||
snrDb: (json['snr'] as num?)?.toDouble(),
|
||||
rssiDbm: (json['rssi'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Durable storage for raw BLE packets so future features can consume
|
||||
/// historical packet bytes across app restarts.
|
||||
class PacketCaptureStorageService {
|
||||
static const String _fileName = 'packet_captures.jsonl';
|
||||
static const int _maxStoredPackets = 20000;
|
||||
|
||||
File? _file;
|
||||
|
||||
Future<File> _resolveFile() async {
|
||||
if (_file != null) return _file!;
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final file = File('${dir.path}/$_fileName');
|
||||
if (!await file.exists()) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
_file = file;
|
||||
return file;
|
||||
}
|
||||
|
||||
Future<void> appendLogs(List<BlePacketLog> logs) async {
|
||||
if (logs.isEmpty) return;
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
final sink = file.openWrite(mode: FileMode.append);
|
||||
for (final log in logs) {
|
||||
final row = StoredPacketCapture(
|
||||
timestamp: log.timestamp,
|
||||
direction: log.direction.name,
|
||||
responseCode: log.responseCode,
|
||||
description: log.description,
|
||||
rawBase64: base64Encode(log.rawData),
|
||||
rawSize: log.rawData.length,
|
||||
snrDb: log.logRxDataInfo?.snrDb,
|
||||
rssiDbm: log.logRxDataInfo?.rssiDbm,
|
||||
);
|
||||
sink.writeln(jsonEncode(row.toJson()));
|
||||
}
|
||||
await sink.flush();
|
||||
await sink.close();
|
||||
await _pruneIfNeeded(file);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [PacketCaptureStorage] Failed to append logs: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<StoredPacketCapture>> loadRecent({int limit = 500}) async {
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
if (!await file.exists()) return const [];
|
||||
final lines = await file.readAsLines();
|
||||
if (lines.isEmpty) return const [];
|
||||
final start = lines.length > limit ? lines.length - limit : 0;
|
||||
return lines
|
||||
.sublist(start)
|
||||
.where((l) => l.trim().isNotEmpty)
|
||||
.map((l) => StoredPacketCapture.fromJson(jsonDecode(l) as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [PacketCaptureStorage] Failed to load recent logs: $e');
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> count() async {
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
if (!await file.exists()) return 0;
|
||||
final lines = await file.readAsLines();
|
||||
return lines.where((l) => l.trim().isNotEmpty).length;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
if (await file.exists()) {
|
||||
await file.writeAsString('');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [PacketCaptureStorage] Failed to clear logs: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pruneIfNeeded(File file) async {
|
||||
final lines = await file.readAsLines();
|
||||
if (lines.length <= _maxStoredPackets) return;
|
||||
final keep = lines.sublist(lines.length - _maxStoredPackets);
|
||||
await file.writeAsString('${keep.join('\n')}\n');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user