2026-08-26 19:03:22 +09:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
2026-08-24 06:43:37 +09:00
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
import 'package:iperf3gui/data/services/iperf3_service.dart';
|
2026-08-26 19:35:10 +09:00
|
|
|
import 'package:iperf3gui/domain/models/iperf3_event.dart';
|
2026-08-24 06:43:37 +09:00
|
|
|
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
2026-08-26 19:03:22 +09:00
|
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
2026-08-26 19:35:10 +09:00
|
|
|
import 'package:iperf3gui/ui/features/test_run/iperf3_event_formatter.dart';
|
2026-08-24 06:43:37 +09:00
|
|
|
|
|
|
|
|
/// State of the test run.
|
|
|
|
|
enum TestRunState { idle, running, completed, error }
|
|
|
|
|
|
|
|
|
|
/// ViewModel for managing test run state.
|
2026-08-26 19:03:22 +09:00
|
|
|
///
|
2026-08-26 19:35:10 +09:00
|
|
|
/// Subscribes to [Iperf3Service.events] so the transcript and the interval
|
|
|
|
|
/// series both grow while the test runs, rather than appearing at the end.
|
2026-08-24 06:43:37 +09:00
|
|
|
class TestRunViewModel extends ChangeNotifier {
|
2026-08-24 06:51:06 +09:00
|
|
|
final Iperf3Service iperf3Service;
|
2026-08-24 06:43:37 +09:00
|
|
|
|
2026-08-26 19:35:10 +09:00
|
|
|
/// How often to notify listeners while output is arriving. iperf3 can report
|
|
|
|
|
/// several times a second across many streams, and each rebuild re-renders
|
|
|
|
|
/// the whole transcript — so repaints are coalesced instead of one per event.
|
|
|
|
|
static const Duration displayInterval = Duration(milliseconds: 250);
|
|
|
|
|
|
2026-08-26 19:03:22 +09:00
|
|
|
TestRunViewModel({required this.iperf3Service}) {
|
2026-08-26 19:35:10 +09:00
|
|
|
_eventSubscription = iperf3Service.events.listen(_onEvent);
|
2026-08-26 19:03:22 +09:00
|
|
|
}
|
|
|
|
|
|
2026-08-26 19:35:10 +09:00
|
|
|
StreamSubscription<Iperf3Event>? _eventSubscription;
|
|
|
|
|
Timer? _repaintTimer;
|
|
|
|
|
bool _dirty = false;
|
2026-08-26 19:03:22 +09:00
|
|
|
|
2026-08-24 06:43:37 +09:00
|
|
|
TestRunState _state = TestRunState.idle;
|
|
|
|
|
TestRunState get state => _state;
|
|
|
|
|
|
|
|
|
|
Iperf3Result? _result;
|
|
|
|
|
Iperf3Result? get result => _result;
|
|
|
|
|
|
|
|
|
|
TestConfiguration? _currentConfig;
|
|
|
|
|
TestConfiguration? get currentConfig => _currentConfig;
|
|
|
|
|
|
2026-08-26 19:35:10 +09:00
|
|
|
final StringBuffer _transcript = StringBuffer();
|
|
|
|
|
|
|
|
|
|
/// Human-readable output accumulated so far.
|
|
|
|
|
String get output => _transcript.toString();
|
|
|
|
|
|
|
|
|
|
final List<Iperf3Interval> _intervals = [];
|
|
|
|
|
|
|
|
|
|
/// Per-interval throughput samples, in arrival order. Structured, so a chart
|
|
|
|
|
/// can be driven from this without re-parsing text.
|
|
|
|
|
List<Iperf3Interval> get intervals => List.unmodifiable(_intervals);
|
|
|
|
|
|
|
|
|
|
Iperf3Start? _start;
|
|
|
|
|
|
|
|
|
|
/// Connection details reported at the start of the run.
|
|
|
|
|
Iperf3Start? get start => _start;
|
2026-08-26 19:03:22 +09:00
|
|
|
|
2026-08-26 19:35:10 +09:00
|
|
|
Iperf3End? _summary;
|
|
|
|
|
|
|
|
|
|
/// Totals reported at the end of the run.
|
|
|
|
|
Iperf3End? get summary => _summary;
|
2026-08-26 19:03:22 +09:00
|
|
|
|
|
|
|
|
/// Whether a test is currently in flight.
|
|
|
|
|
bool get isRunning => _state == TestRunState.running;
|
2026-08-24 06:43:37 +09:00
|
|
|
|
2026-08-26 19:03:59 +09:00
|
|
|
/// Whether the most recent finished run ended because the user stopped it.
|
|
|
|
|
bool get wasStopped => _result?.stopped ?? false;
|
|
|
|
|
|
2026-08-24 06:43:37 +09:00
|
|
|
/// Start a test with the given configuration.
|
|
|
|
|
Future<void> runTest(TestConfiguration config) async {
|
2026-08-26 19:03:22 +09:00
|
|
|
if (_state == TestRunState.running) return;
|
|
|
|
|
|
2026-08-24 06:43:37 +09:00
|
|
|
if (!config.isValid) {
|
2026-08-26 19:35:10 +09:00
|
|
|
_resetRunData();
|
|
|
|
|
_transcript
|
|
|
|
|
.writeln('Invalid configuration: check the server address and port.');
|
2026-08-24 06:43:37 +09:00
|
|
|
_state = TestRunState.error;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 19:35:10 +09:00
|
|
|
_resetRunData();
|
2026-08-24 06:43:37 +09:00
|
|
|
_state = TestRunState.running;
|
|
|
|
|
_currentConfig = config;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
try {
|
2026-08-24 06:51:06 +09:00
|
|
|
final result = await iperf3Service.run(config);
|
2026-08-24 06:43:37 +09:00
|
|
|
_result = result;
|
2026-08-26 19:35:10 +09:00
|
|
|
if (_transcript.isEmpty && result.rawOutput.isNotEmpty) {
|
|
|
|
|
_transcript.write(result.rawOutput);
|
2026-08-26 19:03:22 +09:00
|
|
|
}
|
|
|
|
|
_state =
|
|
|
|
|
result.exitCode == 0 ? TestRunState.completed : TestRunState.error;
|
2026-08-24 06:43:37 +09:00
|
|
|
} catch (e) {
|
2026-08-26 19:35:10 +09:00
|
|
|
_transcript.writeln('Test failed: $e');
|
2026-08-24 06:43:37 +09:00
|
|
|
_state = TestRunState.error;
|
|
|
|
|
} finally {
|
2026-08-26 19:35:10 +09:00
|
|
|
_stopRepaintTimer();
|
2026-08-24 06:43:37 +09:00
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 19:03:22 +09:00
|
|
|
/// Request that a running test stop early.
|
|
|
|
|
Future<void> stopTest() async {
|
|
|
|
|
if (_state != TestRunState.running) return;
|
|
|
|
|
await iperf3Service.stop();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 06:43:37 +09:00
|
|
|
/// Reset to idle state.
|
|
|
|
|
void reset() {
|
2026-08-26 19:35:10 +09:00
|
|
|
_resetRunData();
|
2026-08-24 06:43:37 +09:00
|
|
|
_state = TestRunState.idle;
|
|
|
|
|
_currentConfig = null;
|
2026-08-26 19:03:22 +09:00
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 19:35:10 +09:00
|
|
|
void _resetRunData() {
|
|
|
|
|
_result = null;
|
|
|
|
|
_transcript.clear();
|
|
|
|
|
_intervals.clear();
|
|
|
|
|
_start = null;
|
|
|
|
|
_summary = null;
|
|
|
|
|
_dirty = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onEvent(Iperf3Event event) {
|
|
|
|
|
switch (event) {
|
|
|
|
|
case Iperf3Start e:
|
|
|
|
|
_start = e;
|
|
|
|
|
case Iperf3Interval e:
|
|
|
|
|
// Omitted intervals are warm-up (-O) and are not part of the series.
|
|
|
|
|
if (!e.omitted) _intervals.add(e);
|
|
|
|
|
case Iperf3End e:
|
|
|
|
|
_summary = e;
|
|
|
|
|
case Iperf3Error _:
|
|
|
|
|
case Iperf3Stopped _:
|
|
|
|
|
case Iperf3LogLine _:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final line = formatIperf3Event(event);
|
|
|
|
|
if (line != null && line.isNotEmpty) {
|
|
|
|
|
_transcript.writeln(line);
|
|
|
|
|
}
|
|
|
|
|
_scheduleRepaint();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Coalesce repaints: mark dirty and let a timer flush at [displayInterval].
|
|
|
|
|
void _scheduleRepaint() {
|
|
|
|
|
_dirty = true;
|
|
|
|
|
_repaintTimer ??= Timer.periodic(displayInterval, (_) {
|
|
|
|
|
if (!_dirty) return;
|
|
|
|
|
_dirty = false;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _stopRepaintTimer() {
|
|
|
|
|
_repaintTimer?.cancel();
|
|
|
|
|
_repaintTimer = null;
|
|
|
|
|
_dirty = false;
|
2026-08-24 06:43:37 +09:00
|
|
|
}
|
2026-08-26 19:03:22 +09:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
2026-08-26 19:35:10 +09:00
|
|
|
_stopRepaintTimer();
|
|
|
|
|
_eventSubscription?.cancel();
|
2026-08-26 19:03:22 +09:00
|
|
|
iperf3Service.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
2026-08-24 06:43:37 +09:00
|
|
|
}
|