Files
ulapp/FieldLogger/ViewModels/DebugViewModel.cs
2026-07-10 20:48:42 -05:00

290 lines
8.7 KiB
C#

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<DebugMessage> 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<string>();
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;
}
}