using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FieldLogger.Models; using FieldLogger.Services; namespace FieldLogger.ViewModels; public sealed partial class DebugViewModel : ObservableObject { private readonly UmReceiverService _receiver; private readonly MaglinkService _gps; private readonly DeviceConnectionManager _manager; [ObservableProperty] private bool _isTimedLoggingEnabled; [ObservableProperty] private int _logPeriod = 10; // 10 = 1000ms (10 * 100ms) [ObservableProperty] private string _timedLoggingStatus = "Disabled"; [ObservableProperty] private bool _autoScroll = true; [ObservableProperty] private bool _showHex = false; [ObservableProperty] private int _messageCount; [ObservableProperty] private bool _isConnected; [ObservableProperty] private string _connectionStatus = "Not connected"; [ObservableProperty] private string _connectionStateColor = "Gray"; public ObservableCollection Messages { get; } = new(); public string LogPeriodLabel => $"Period: {LogPeriod * 100}ms ({(LogPeriod * 100 / 1000.0):F1}s)"; public DebugViewModel(UmReceiverService receiver, DeviceConnectionManager manager) { _receiver = receiver; _manager = manager; _gps = manager.Gps; } public void OnAppearing() { _receiver.LineReceived += OnUmLineReceived; _gps.LineReceived += OnGpsLineReceived; UpdateConnectionStatus(); } public void OnDisappearing() { _receiver.LineReceived -= OnUmLineReceived; _gps.LineReceived -= OnGpsLineReceived; } private void UpdateConnectionStatus() { IsConnected = _receiver.IsConnected; var connections = new List(); if (_receiver.IsConnected) connections.Add($"Locator: {_receiver.DeviceName ?? "receiver"}"); if (_gps.IsConnected) connections.Add($"RTK: {_gps.DeviceName ?? "receiver"}"); ConnectionStatus = connections.Count > 0 ? string.Join(" · ", connections) : "No devices connected"; ConnectionStateColor = connections.Count > 0 ? "Green" : "Gray"; } [RelayCommand] private async Task EnablePushButtonLoggingAsync() { try { await _receiver.EnableLoggingAsync(); AddMessage("TX", "$UMPBDL,1", "Command"); } catch (Exception ex) { await Shell.Current.DisplayAlert("Error", $"Failed to enable logging: {ex.Message}", "OK"); } } partial void OnIsTimedLoggingEnabledChanged(bool value) { _ = UpdateTimedLoggingAsync(value); } partial void OnLogPeriodChanged(int value) { OnPropertyChanged(nameof(LogPeriodLabel)); if (IsTimedLoggingEnabled) { _ = UpdateTimedLoggingAsync(true); } } private async Task UpdateTimedLoggingAsync(bool enabled) { try { if (!_receiver.IsConnected) { TimedLoggingStatus = "Not connected to receiver"; IsTimedLoggingEnabled = false; return; } if (enabled) { await _receiver.EnableTimedLoggingAsync(LogPeriod); TimedLoggingStatus = $"Enabled - {LogPeriod * 100}ms interval"; AddMessage("TX", $"$UMETDL,{LogPeriod}", "Command"); } else { await _receiver.DisableTimedLoggingAsync(); TimedLoggingStatus = "Disabled"; AddMessage("TX", "$UMETDL,0", "Command"); } } catch (Exception ex) { TimedLoggingStatus = $"Error: {ex.Message}"; IsTimedLoggingEnabled = false; } } private void OnUmLineReceived(object? sender, string line) => AddReceivedLine(line, false); private void OnGpsLineReceived(object? sender, string line) => AddReceivedLine(line, true); private void AddReceivedLine(string line, bool isGps) { MainThread.BeginInvokeOnMainThread(() => { var messageType = isGps ? ClassifyGpsMessage(line) : ClassifyMessage(line); var parsedData = ParseMessage(line, messageType); AddMessage("RX", line, messageType, parsedData); }); } private static string ClassifyGpsMessage(string line) { if (line.StartsWith("$GNPOS,", StringComparison.Ordinal)) return NmeaSentence.VerifyChecksum(line) ? "GNPOS" : "GNPOS (bad checksum)"; if (line.StartsWith("$GNDEV,", StringComparison.Ordinal)) return NmeaSentence.VerifyChecksum(line) ? "GNDEV" : "GNDEV (bad checksum)"; return line.StartsWith('$') ? "NMEA" : "RTK Text"; } private void AddMessage(string direction, string rawMessage, string type, string? parsedData = null) { var message = new DebugMessage { Timestamp = DateTime.Now, Direction = direction, RawMessage = rawMessage, Type = type, ParsedData = parsedData, TypeColor = GetTypeColor(type) }; Messages.Add(message); MessageCount = Messages.Count; // Keep last 500 messages while (Messages.Count > 500) { Messages.RemoveAt(0); } // TODO: Auto-scroll if enabled } private static string ClassifyMessage(string line) { line = line.Trim(); // Try parsing as UmLogPacket (data packet) var packet = UmLogPacket.TryParse(line); if (packet is not null) { return packet.LogType.HasValue ? "UMLOG (Timed)" : "UMLOG (Button)"; } // Try parsing as device info var info = UmDeviceInfo.TryParse(line); if (info is not null) return "UMDEV"; if (line.Equals("OK", StringComparison.OrdinalIgnoreCase)) return "OK"; if (line.Equals("ERROR", StringComparison.OrdinalIgnoreCase)) return "ERROR"; if (line.StartsWith("$", StringComparison.Ordinal)) return "Unknown"; return "Text"; } private static string? ParseMessage(string line, string type) { if (type.StartsWith("UMLOG")) { var packet = UmLogPacket.TryParse(line); if (packet is not null) { var logTypeStr = packet.LogType.HasValue ? $"[{packet.LogType}] " : ""; return $"{logTypeStr}{packet.Mode}, {packet.Frequency}Hz, Signal={packet.Signal}, " + $"Depth={packet.DepthRaw}, Current={packet.CurrentRaw}, Battery={packet.BatteryVoltage:F2}V"; } } else if (type == "UMDEV") { var info = UmDeviceInfo.TryParse(line); if (info is not null) { return $"Mfr={info.Manufacturer}, Model={info.ModelName}, SN={info.SerialNumber}, FW={info.SoftwareVersion}"; } } else if (type == "GNPOS") { var fix = GnssFix.TryParse(line); if (fix is not null) return $"{fix.StatusLabel}, {fix.Latitude:F8}, {fix.Longitude:F8}, " + $"H ±{fix.Hrms:F3}m, V ±{fix.Vrms:F3}m, {fix.SatellitesUsed} sats"; } else if (type == "GNDEV") { var info = GnssDeviceInfo.TryParse(line); if (info is not null) return $"SN={info.SerialNumber}, PCB={info.PcbVersion}, FW={info.FirmwareVersion}"; } return null; } private static string GetModeText(string mode) { return mode switch { "0" => "Single", "1" => "Twin", "2" => "Null", "3" => "Sweep", "4" => "Twin Sweep", "5" => "Omni", "6" => "Twin Omni", _ => $"Mode {mode}" }; } private static string GetTypeColor(string type) { return type switch { "OK" => "Green", "ERROR" => "Red", "UMLOG (Button)" => "Blue", "UMLOG (Timed)" => "DodgerBlue", "UMDEV" => "Purple", "GNPOS" => "ForestGreen", "GNDEV" => "DarkViolet", _ when type.Contains("bad checksum", StringComparison.Ordinal) => "Red", "NMEA" => "Teal", "RTK Text" => "DarkOrange", "Command" => "Orange", _ when type.StartsWith("UMLOG") => "Blue", _ => "Gray" }; } [RelayCommand] private void ClearMessages() { Messages.Clear(); MessageCount = 0; } }