using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using FieldLogger.Models; using FieldLogger.Services; namespace FieldLogger.ViewModels; /// /// Shared, glanceable hardware status shown above each primary workflow. The timer marks /// position data stale even when the receiver stops sending without disconnecting. /// public sealed partial class AppStatusViewModel : ObservableObject { private static readonly TimeSpan StaleAfter = TimeSpan.FromSeconds(2); private readonly DeviceConnectionManager _manager; private readonly MaglinkService _gps; [ObservableProperty] private ConnectionState _locatorState; [ObservableProperty] private ConnectionState _gnssState; [ObservableProperty] private string _locatorStatus = "Locator disconnected"; [ObservableProperty] private string _gnssStatus = "GNSS disconnected"; [ObservableProperty] private string _positionSummary = "No GNSS fix · accuracy —"; public AppStatusViewModel(DeviceConnectionManager manager, MaglinkService gps) { _manager = manager; _gps = gps; _manager.PropertyChanged += OnConnectionPropertyChanged; _gps.FixReceived += OnFixReceived; Refresh(); } private void OnConnectionPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(DeviceConnectionManager.LocatorState) or nameof(DeviceConnectionManager.GpsState) or nameof(DeviceConnectionManager.LocatorName) or nameof(DeviceConnectionManager.GpsName)) { Refresh(); } } private void OnFixReceived(object? sender, GnssFix fix) => Refresh(); /// Refreshes status safely from device callbacks or a UI dispatcher timer. public void Refresh() { var dispatcher = Application.Current?.Dispatcher; if (dispatcher?.IsDispatchRequired == true) { dispatcher.Dispatch(RefreshCore); return; } RefreshCore(); } private void RefreshCore() { LocatorState = _manager.LocatorState; GnssState = _manager.GpsState; LocatorStatus = DeviceLabel("Locator", _manager.LocatorName, LocatorState); GnssStatus = DeviceLabel("GNSS", _manager.GpsName, GnssState); var fix = _gps.LatestFix; if (fix is null) { PositionSummary = GnssState == ConnectionState.Connected ? "Waiting for fix · accuracy —" : "No GNSS fix · accuracy —"; return; } var stale = DateTime.UtcNow - fix.ReceivedUtc > StaleAfter; var staleLabel = stale ? " · STALE" : string.Empty; PositionSummary = $"{fix.StatusLabel}{staleLabel} · H ±{fix.Hrms:F3} m · " + $"V ±{fix.Vrms:F3} m · {fix.SatellitesUsed} sats · corr {fix.CorrectionAgeSeconds:F1} s"; } private static string DeviceLabel(string kind, string? name, ConnectionState state) { var stateText = state switch { ConnectionState.Connected => "connected", ConnectionState.Connecting => "connecting", _ => "disconnected", }; return state == ConnectionState.Disconnected || string.IsNullOrWhiteSpace(name) ? $"{kind} {stateText}" : $"{kind} {stateText} · {name}"; } }