First working interface

This commit is contained in:
2026-04-21 19:38:20 -03:00
parent 9efd27afa5
commit dbf5e5c1ac
42 changed files with 1211 additions and 95 deletions

View File

@@ -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) {

View File

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

View File

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