First working interface
This commit is contained in:
@@ -14,15 +14,15 @@ class ExportResult {
|
||||
final int bytesWritten;
|
||||
}
|
||||
|
||||
/// Builds CSV row strings. Shared by both platform implementations so the
|
||||
/// row layout is defined exactly once.
|
||||
class CsvRowBuilder {
|
||||
CsvRowBuilder(this.layout);
|
||||
/// Builds rows for the data CSV. Needs a layout to know which channels are
|
||||
/// enabled and what their column names should be.
|
||||
class CsvDataRowBuilder {
|
||||
CsvDataRowBuilder(this.layout);
|
||||
final LayoutController layout;
|
||||
|
||||
/// Header row for data CSV. Disabled channels are omitted; pause and the
|
||||
/// 8 status fields always appear.
|
||||
String dataHeader() {
|
||||
/// Header row. Disabled channels are omitted; pause and the 8 status
|
||||
/// fields always appear.
|
||||
String header() {
|
||||
final cols = <String>['timestamp_us'];
|
||||
for (var ch = 1; ch <= 16; ch++) {
|
||||
if (layout.configFor(ch).enabled) {
|
||||
@@ -37,7 +37,7 @@ class CsvRowBuilder {
|
||||
}
|
||||
|
||||
/// Row for one packet. Missing values are blank cells.
|
||||
String dataRow(packet) {
|
||||
String row(packet) {
|
||||
final cells = <String>[];
|
||||
cells.add(packet.hasTimestampUs() ? packet.timestampUs.toString() : '');
|
||||
for (var ch = 1; ch <= 16; ch++) {
|
||||
@@ -51,16 +51,6 @@ class CsvRowBuilder {
|
||||
return cells.join(',');
|
||||
}
|
||||
|
||||
String logHeader() => 'timestamp_us,severity,error_number,description';
|
||||
|
||||
String logRow(entry) {
|
||||
final ts = entry.hasTimestampUs() ? entry.timestampUs.toString() : '';
|
||||
final sev = entry.hasSeverity() ? entry.severity.name : '';
|
||||
final err = entry.hasErrorNumber() ? entry.errorNumber.toString() : '';
|
||||
final desc = entry.hasDescription() ? _escape(entry.description) : '';
|
||||
return '$ts,$sev,$err,$desc';
|
||||
}
|
||||
|
||||
static String _channelCell(packet, int ch) {
|
||||
switch (ch) {
|
||||
case 1: return packet.hasCh1() ? packet.ch1.toString() : '';
|
||||
@@ -99,6 +89,22 @@ class CsvRowBuilder {
|
||||
|
||||
/// Strip characters that would break CSV column names.
|
||||
static String _safe(String s) => s.replaceAll(RegExp(r'[,\s]+'), '_');
|
||||
}
|
||||
|
||||
/// Builds rows for the log CSV. Takes no layout — log output is
|
||||
/// fully determined by the `LogPacket` itself.
|
||||
class CsvLogRowBuilder {
|
||||
const CsvLogRowBuilder();
|
||||
|
||||
String header() => 'timestamp_us,severity,error_number,description';
|
||||
|
||||
String row(entry) {
|
||||
final ts = entry.hasTimestampUs() ? entry.timestampUs.toString() : '';
|
||||
final sev = entry.hasSeverity() ? entry.severity.name : '';
|
||||
final err = entry.hasErrorNumber() ? entry.errorNumber.toString() : '';
|
||||
final desc = entry.hasDescription() ? _escape(entry.description) : '';
|
||||
return '$ts,$sev,$err,$desc';
|
||||
}
|
||||
|
||||
/// Escape a free-form field for CSV. Wraps in quotes if needed.
|
||||
static String _escape(String s) {
|
||||
|
||||
@@ -20,16 +20,16 @@ class CsvExporterImpl implements CsvExporter {
|
||||
required LayoutController layout,
|
||||
ProgressCallback? onProgress,
|
||||
}) async {
|
||||
final builder = CsvRowBuilder(layout);
|
||||
final builder = CsvDataRowBuilder(layout);
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final ts = DateTime.now().toIso8601String().replaceAll(':', '-');
|
||||
final file = File('${dir.path}/telemetry_data_$ts.csv');
|
||||
final sink = file.openWrite();
|
||||
sink.writeln(builder.dataHeader());
|
||||
sink.writeln(builder.header());
|
||||
final total = buffer.length;
|
||||
var written = 0;
|
||||
for (var i = 0; i < total; i++) {
|
||||
sink.writeln(builder.dataRow(buffer[i]));
|
||||
sink.writeln(builder.row(buffer[i]));
|
||||
written++;
|
||||
if (written % _chunkRows == 0) {
|
||||
onProgress?.call(written / total);
|
||||
@@ -51,17 +51,16 @@ class CsvExporterImpl implements CsvExporter {
|
||||
required LogBuffer buffer,
|
||||
ProgressCallback? onProgress,
|
||||
}) async {
|
||||
// Logs don't need a layout to format (no column projection).
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final ts = DateTime.now().toIso8601String().replaceAll(':', '-');
|
||||
final file = File('${dir.path}/telemetry_log_$ts.csv');
|
||||
final sink = file.openWrite();
|
||||
final builder = CsvRowBuilder(_NoLayoutPlaceholder());
|
||||
sink.writeln(builder.logHeader());
|
||||
const builder = CsvLogRowBuilder();
|
||||
sink.writeln(builder.header());
|
||||
final total = buffer.length;
|
||||
var written = 0;
|
||||
for (final entry in buffer.iterate()) {
|
||||
sink.writeln(builder.logRow(entry));
|
||||
sink.writeln(builder.row(entry));
|
||||
written++;
|
||||
if (written % _chunkRows == 0) {
|
||||
onProgress?.call(written / total);
|
||||
@@ -77,10 +76,3 @@ class CsvExporterImpl implements CsvExporter {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// CsvRowBuilder.logRow doesn't actually need a layout. We pass a placeholder
|
||||
/// so we don't have to plumb a real one through for log-only exports.
|
||||
class _NoLayoutPlaceholder implements LayoutController {
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
@@ -22,14 +22,14 @@ class CsvExporterImpl implements CsvExporter {
|
||||
required LayoutController layout,
|
||||
ProgressCallback? onProgress,
|
||||
}) async {
|
||||
final builder = CsvRowBuilder(layout);
|
||||
final builder = CsvDataRowBuilder(layout);
|
||||
final chunks = <String>[];
|
||||
chunks.add('${builder.dataHeader()}\n');
|
||||
chunks.add('${builder.header()}\n');
|
||||
final total = buffer.length;
|
||||
final sb = StringBuffer();
|
||||
var written = 0;
|
||||
for (var i = 0; i < total; i++) {
|
||||
sb.writeln(builder.dataRow(buffer[i]));
|
||||
sb.writeln(builder.row(buffer[i]));
|
||||
written++;
|
||||
if (written % _chunkRows == 0) {
|
||||
chunks.add(sb.toString());
|
||||
@@ -53,14 +53,14 @@ class CsvExporterImpl implements CsvExporter {
|
||||
required LogBuffer buffer,
|
||||
ProgressCallback? onProgress,
|
||||
}) async {
|
||||
final builder = CsvRowBuilder(_NoLayoutPlaceholder());
|
||||
const builder = CsvLogRowBuilder();
|
||||
final chunks = <String>[];
|
||||
chunks.add('${builder.logHeader()}\n');
|
||||
chunks.add('${builder.header()}\n');
|
||||
final total = buffer.length;
|
||||
final sb = StringBuffer();
|
||||
var written = 0;
|
||||
for (final entry in buffer.iterate()) {
|
||||
sb.writeln(builder.logRow(entry));
|
||||
sb.writeln(builder.row(entry));
|
||||
written++;
|
||||
if (written % _chunkRows == 0) {
|
||||
chunks.add(sb.toString());
|
||||
@@ -104,8 +104,3 @@ class CsvExporterImpl implements CsvExporter {
|
||||
html.Url.revokeObjectUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoLayoutPlaceholder implements LayoutController {
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
Reference in New Issue
Block a user