import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:iperf3gui/domain/models/iperf3_event.dart'; import 'package:iperf3gui/domain/models/iperf3_result.dart'; import 'package:iperf3gui/domain/models/test_configuration.dart'; import 'package:iperf3gui/domain/services/iperf3_runner.dart'; import 'package:iperf3gui/ui/features/test_run/iperf3_event_formatter.dart'; /// State of the test run. enum TestRunState { idle, running, completed, error } /// ViewModel for managing test run state. /// /// Subscribes to [Iperf3Runner.events] so the transcript and the interval /// series both grow while the test runs, rather than appearing at the end. class TestRunViewModel extends ChangeNotifier { final Iperf3Runner iperf3Service; /// 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); TestRunViewModel({required this.iperf3Service}) { _eventSubscription = iperf3Service.events.listen(_onEvent); } StreamSubscription? _eventSubscription; Timer? _repaintTimer; bool _dirty = false; TestRunState _state = TestRunState.idle; TestRunState get state => _state; Iperf3Result? _result; Iperf3Result? get result => _result; TestConfiguration? _currentConfig; TestConfiguration? get currentConfig => _currentConfig; final StringBuffer _transcript = StringBuffer(); /// Human-readable output accumulated so far. String get output => _transcript.toString(); final List _intervals = []; /// Per-interval throughput samples, in arrival order. Structured, so a chart /// can be driven from this without re-parsing text. List get intervals => List.unmodifiable(_intervals); Iperf3Start? _start; /// Connection details reported at the start of the run. Iperf3Start? get start => _start; Iperf3End? _summary; /// Totals reported at the end of the run. Iperf3End? get summary => _summary; /// Whether a test is currently in flight. bool get isRunning => _state == TestRunState.running; /// Whether the most recent finished run ended because the user stopped it. bool get wasStopped => _result?.stopped ?? false; /// Start a test with the given configuration. Future runTest(TestConfiguration config) async { if (_state == TestRunState.running) return; if (!config.isValid) { _resetRunData(); _transcript .writeln('Invalid configuration: check the server address and port.'); _state = TestRunState.error; notifyListeners(); return; } _resetRunData(); _state = TestRunState.running; _currentConfig = config; notifyListeners(); try { final result = await iperf3Service.run(config); _result = result; if (_transcript.isEmpty && result.rawOutput.isNotEmpty) { _transcript.write(result.rawOutput); } _state = result.exitCode == 0 ? TestRunState.completed : TestRunState.error; } catch (e) { _transcript.writeln('Test failed: $e'); _state = TestRunState.error; } finally { _stopRepaintTimer(); notifyListeners(); } } /// Request that a running test stop early. Future stopTest() async { if (_state != TestRunState.running) return; await iperf3Service.stop(); } /// Reset to idle state. void reset() { _resetRunData(); _state = TestRunState.idle; _currentConfig = null; notifyListeners(); } 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; } @override void dispose() { _stopRepaintTimer(); _eventSubscription?.cancel(); iperf3Service.dispose(); super.dispose(); } }