import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import '../config/settings.dart'; import '../decoder/decoder.dart'; import '../proto/messages.pb.dart'; import '../transport/connection_state.dart'; import '../transport/websocket_transport.dart'; import 'decimator.dart'; import 'log_buffer.dart'; import 'packet_buffer.dart'; import 'pps_counter.dart'; import 'status_snapshot.dart'; import 'view_state.dart'; /// Central state owner. Wires transport → decoder → buffers → notifiers. /// /// Lifecycle: construct → call `start()` → use → call `dispose()`. class SessionController { SessionController({ required this.transport, required this.decoder, required this.settings, }) : packets = PacketBuffer(capacity: settings.packetBufferCapacity), logs = LogBuffer(capacity: settings.logBufferCapacity), viewState = ViewState(); final WebSocketTransport transport; final Decoder decoder; final Settings settings; final PacketBuffer packets; final LogBuffer logs; final ViewState viewState; final Decimator decimator = Decimator(); final PpsCounter _pps = PpsCounter(); final ValueNotifier _frameTick = ValueNotifier(0); final ValueNotifier _logTick = ValueNotifier(0); final ValueNotifier _snapshot = ValueNotifier(StatusSnapshot.empty()); final ValueNotifier cursorEnabled = ValueNotifier(false); ValueListenable get frameTick => _frameTick; ValueListenable get logTick => _logTick; ValueListenable get statusSnapshot => _snapshot; ValueListenable get connectionState => transport.state; StreamSubscription? _envSub; Ticker? _ticker; Future start() async { _envSub = decoder.envelopes.listen(_onEnvelope); transport.frames.listen((bytes) => decoder.feed(bytes)); _ticker = Ticker(_onTick)..start(); await transport.connect(settings.wsUrl); } void _onEnvelope(Envelope env) { if (env.hasData()) { packets.add(env.data); _pps.recordArrival(); decimator.invalidateTail(); } else if (env.hasLog()) { logs.add(env.log); _logTick.value++; } } void _onTick(Duration elapsed) { _snapshot.value = _computeSnapshot(); _frameTick.value++; } StatusSnapshot _computeSnapshot() { final lookback = settings.statusLookback; final statusValues = List.filled(8, null); bool? protoPaused; int resolved = 0; // Walk backward from newest, up to `lookback` packets, stop when all 9 // fields are resolved. final n = packets.length; final scan = lookback < n ? lookback : n; for (var i = 0; i < scan && resolved < 9; i++) { final p = packets[n - 1 - i]; for (var s = 0; s < 8; s++) { if (statusValues[s] == null && _hasStatus(p, s + 1)) { statusValues[s] = _getStatus(p, s + 1); resolved++; } } if (protoPaused == null && p.hasPause()) { protoPaused = p.pause; resolved++; } } return StatusSnapshot( connection: transport.state.value, pps: _pps.current(), statusValues: statusValues, protoPaused: protoPaused, ); } bool get isPaused => viewState.userPaused || (_snapshot.value.protoPaused ?? false); PauseSource get pauseSource { final u = viewState.userPaused; final p = _snapshot.value.protoPaused ?? false; if (u && p) return PauseSource.both; if (u) return PauseSource.user; if (p) return PauseSource.proto; return PauseSource.none; } void clearAll() { packets.clear(); logs.clear(); decimator.clear(); _pps.reset(); viewState.goLive(); _logTick.value++; } Future reconnect() async { await transport.connect(settings.wsUrl); } Future dispose() async { _ticker?.dispose(); _ticker = null; await _envSub?.cancel(); _envSub = null; _frameTick.dispose(); _logTick.dispose(); _snapshot.dispose(); cursorEnabled.dispose(); await decoder.dispose(); await transport.dispose(); viewState.dispose(); } // ---- field accessors (mirrored from Decimator for status fields) ---- static bool _hasStatus(DataPacket p, int idx) { switch (idx) { case 1: return p.hasStatus1(); case 2: return p.hasStatus2(); case 3: return p.hasStatus3(); case 4: return p.hasStatus4(); case 5: return p.hasStatus5(); case 6: return p.hasStatus6(); case 7: return p.hasStatus7(); case 8: return p.hasStatus8(); default: return false; } } static int _getStatus(DataPacket p, int idx) { switch (idx) { case 1: return p.status1; case 2: return p.status2; case 3: return p.status3; case 4: return p.status4; case 5: return p.status5; case 6: return p.status6; case 7: return p.status7; case 8: return p.status8; default: return 0; } } }