diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 56a3277..1cebe43 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,10 @@ "permissions": { "allow": [ "Bash(git init)", - "Bash(git add .)" + "Bash(git add .)", + "Bash(git commit -m \"$(cat <<''EOF''\nInitial commit: FieldLogger MAUI app with Maglink BLE support\n\nAdded Maglink RTK GNSS receiver integration with correct BLE UUIDs and device name filtering (ML-* prefix).\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")", + "Read(//d/maglink/**)", + "Bash(git add -A)" ], "deny": [], "ask": [] diff --git a/FieldLogger/App.xaml.cs b/FieldLogger/App.xaml.cs index 1711798..691ecbc 100644 --- a/FieldLogger/App.xaml.cs +++ b/FieldLogger/App.xaml.cs @@ -1,14 +1,57 @@ -namespace FieldLogger; +using FieldLogger.Services; + +namespace FieldLogger; public partial class App : Application { - public App() + private readonly DeviceConnectionManager _connectionManager; + + public App(DeviceConnectionManager connectionManager) { InitializeComponent(); + _connectionManager = connectionManager; + + Console.WriteLine("===== FIELD LOGGER APP STARTING ====="); + Console.WriteLine($"Console output is working! Time: {DateTime.Now:HH:mm:ss}"); } protected override Window CreateWindow(IActivationState? activationState) { - return new Window(new AppShell()); + var window = new Window(new AppShell()); + + // Disconnect BLE devices when window is closing + window.Destroying += async (s, e) => + { + Console.WriteLine("App: Window closing, disconnecting devices..."); + await DisconnectAllDevicesAsync(); + }; + + return window; + } + + private async Task DisconnectAllDevicesAsync() + { + try + { + // Disconnect locator + if (_connectionManager.Locator.IsConnected) + { + Console.WriteLine("App: Disconnecting locator..."); + await _connectionManager.Locator.DisconnectAsync(); + } + + // Disconnect GPS + if (_connectionManager.Gps.IsConnected) + { + Console.WriteLine("App: Disconnecting GPS..."); + await _connectionManager.Gps.DisconnectAsync(); + } + + Console.WriteLine("App: All devices disconnected"); + } + catch (Exception ex) + { + Console.WriteLine($"App: Error disconnecting devices: {ex.Message}"); + } } } \ No newline at end of file diff --git a/FieldLogger/AppShell.xaml b/FieldLogger/AppShell.xaml index 48f1b29..d2b98a2 100644 --- a/FieldLogger/AppShell.xaml +++ b/FieldLogger/AppShell.xaml @@ -10,6 +10,7 @@ + diff --git a/FieldLogger/MauiProgram.cs b/FieldLogger/MauiProgram.cs index a0a4bfa..4545bf3 100644 --- a/FieldLogger/MauiProgram.cs +++ b/FieldLogger/MauiProgram.cs @@ -42,6 +42,7 @@ public static class MauiProgram builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); @@ -50,6 +51,7 @@ public static class MauiProgram builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/FieldLogger/Models/DebugMessage.cs b/FieldLogger/Models/DebugMessage.cs new file mode 100644 index 0000000..aa25592 --- /dev/null +++ b/FieldLogger/Models/DebugMessage.cs @@ -0,0 +1,24 @@ +namespace FieldLogger.Models; + +public class DebugMessage +{ + public DateTime Timestamp { get; set; } + public string Direction { get; set; } = ""; + public string RawMessage { get; set; } = ""; + public string Type { get; set; } = ""; + public string? ParsedData { get; set; } + public string TypeColor { get; set; } = "Gray"; + public bool HasParsedData => !string.IsNullOrEmpty(ParsedData); + + public string HexBytes + { + get + { + if (string.IsNullOrEmpty(RawMessage)) + return "[empty]"; + + var bytes = System.Text.Encoding.ASCII.GetBytes(RawMessage); + return BitConverter.ToString(bytes).Replace("-", " "); + } + } +} diff --git a/FieldLogger/Models/UmLogPacket.cs b/FieldLogger/Models/UmLogPacket.cs index 590fe18..3d8a094 100644 --- a/FieldLogger/Models/UmLogPacket.cs +++ b/FieldLogger/Models/UmLogPacket.cs @@ -10,12 +10,16 @@ public enum UmUtility { None = 0, Gas = 1, Power = 2, Communications = 3, Water public enum UmBatteryType { NotMeasured = 0, Alkaline = 1, AlkalineError = 2, Lithium = 3 } +public enum UmLogType { PushButton = 0, TimeBased = 1 } + /// -/// A single push-button data-log packet from the UM locating receiver -/// (PBDL schema 1, 21 comma-delimited fields). +/// A single data-log packet from the UM locating receiver. +/// Schema 1 (push-button): 21 fields +/// Schema 2 (timed logging): 22 fields with LogType as first field /// public sealed class UmLogPacket { + public UmLogType? LogType { get; init; } // Schema 2 only public UmMode Mode { get; init; } public int Frequency { get; init; } public UmFreqType FreqType { get; init; } @@ -39,7 +43,8 @@ public sealed class UmLogPacket public bool MenuInUse { get; init; } public string RawText { get; init; } = ""; - public const int FieldCount = 21; + public const int Schema1FieldCount = 21; + public const int Schema2FieldCount = 22; /// Depth converted to meters, when the raw string can be interpreted; otherwise null. public double? DepthMeters => TryParseDepthMeters(DepthRaw); @@ -90,23 +95,45 @@ public sealed class UmLogPacket } /// - /// Parses a 21-field PBDL schema 1 packet. Returns null if the line does not look like a data packet. + /// Parses either Schema 1 (21 fields, push-button) or Schema 2 (22 fields, timed logging). + /// Returns null if the line does not look like a data packet. /// public static UmLogPacket? TryParse(string line) { var fields = line.Trim().Split(','); - if (fields.Length < FieldCount) - return null; - // First field must be a small integer (Mode 0-6) to qualify as a data packet. - if (!int.TryParse(fields[0], out var mode) || mode is < 0 or > 6) - return null; + // Try Schema 2 (22 fields with LogType) + if (fields.Length >= Schema2FieldCount) + { + if (int.TryParse(fields[0], out var logType) && logType is 0 or 1) + { + if (int.TryParse(fields[1], out var mode) && mode is >= 0 and <= 6) + { + return ParseSchema2(fields, (UmLogType)logType, line); + } + } + } + // Try Schema 1 (21 fields without LogType) + if (fields.Length >= Schema1FieldCount) + { + if (int.TryParse(fields[0], out var mode) && mode is >= 0 and <= 6) + { + return ParseSchema1(fields, line); + } + } + + return null; + } + + private static UmLogPacket? ParseSchema1(string[] fields, string line) + { try { return new UmLogPacket { - Mode = (UmMode)mode, + LogType = null, // Schema 1 doesn't have LogType + Mode = (UmMode)ParseInt(fields[0]), Frequency = ParseInt(fields[1]), FreqType = (UmFreqType)ParseInt(fields[2]), Signal = ParseInt(fields[3]), @@ -136,6 +163,44 @@ public sealed class UmLogPacket } } + private static UmLogPacket? ParseSchema2(string[] fields, UmLogType logType, string line) + { + try + { + // Schema 2 has LogType at index 0, then same fields as Schema 1 shifted by 1 + return new UmLogPacket + { + LogType = logType, + Mode = (UmMode)ParseInt(fields[1]), + Frequency = ParseInt(fields[2]), + FreqType = (UmFreqType)ParseInt(fields[3]), + Signal = ParseInt(fields[4]), + GainDb = ParseInt(fields[5]), + DepthRaw = fields[6].Trim(), + CurrentRaw = fields[7].Trim(), + CompassAngle = ParseInt(fields[8]), + GuidanceArrows = ParseInt(fields[9]), + LdPhase = ParseInt(fields[10]), + Clipping = ParseInt(fields[11]) != 0, + DepthCurrentSetting = ParseInt(fields[12]), + LrArrowStyle = ParseInt(fields[13]), + AudioVolume = ParseInt(fields[14]), + AudioModulation = ParseInt(fields[15]), + AudioSound = ParseInt(fields[16]), + BatteryVoltage = ParseDouble(fields[17]), + BatteryType = (UmBatteryType)ParseInt(fields[18]), + Backlight = ParseInt(fields[19]), + Utility = (UmUtility)ParseInt(fields[20]), + MenuInUse = ParseInt(fields[21]) != 0, + RawText = line.Trim(), + }; + } + catch (FormatException) + { + return null; + } + } + private static int ParseInt(string s) => int.Parse(s.Trim(), CultureInfo.InvariantCulture); private static double ParseDouble(string s) => double.Parse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture); } diff --git a/FieldLogger/Platforms/Android/AndroidManifest.xml b/FieldLogger/Platforms/Android/AndroidManifest.xml index 0869368..2603a9b 100644 --- a/FieldLogger/Platforms/Android/AndroidManifest.xml +++ b/FieldLogger/Platforms/Android/AndroidManifest.xml @@ -2,7 +2,7 @@ - + diff --git a/FieldLogger/Platforms/Windows/App.xaml.cs b/FieldLogger/Platforms/Windows/App.xaml.cs index 79ca7e5..09b41f5 100644 --- a/FieldLogger/Platforms/Windows/App.xaml.cs +++ b/FieldLogger/Platforms/Windows/App.xaml.cs @@ -1,4 +1,5 @@ using Microsoft.UI.Xaml; +using System.Runtime.InteropServices; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -10,6 +11,10 @@ namespace FieldLogger.WinUI; /// public partial class App : MauiWinUIApplication { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool AllocConsole(); + /// /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). @@ -17,6 +22,12 @@ public partial class App : MauiWinUIApplication public App() { this.InitializeComponent(); + + // Allocate console window for debug output +#if DEBUG + AllocConsole(); + Console.WriteLine("===== DEBUG CONSOLE ALLOCATED ====="); +#endif } protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); diff --git a/FieldLogger/Services/Ble/BleSerialClient.cs b/FieldLogger/Services/Ble/BleSerialClient.cs index 9bfe960..3417f85 100644 --- a/FieldLogger/Services/Ble/BleSerialClient.cs +++ b/FieldLogger/Services/Ble/BleSerialClient.cs @@ -42,10 +42,14 @@ public sealed class BleSerialClient : IAsyncDisposable { await DisconnectAsync().ConfigureAwait(false); + // Add a timeout to prevent hanging indefinitely when device is not available + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(10)); + var device = await _adapter.ConnectToKnownDeviceAsync( deviceId, new ConnectParameters(autoConnect: false, forceBleTransport: true), - cancellationToken).ConfigureAwait(false); + timeoutCts.Token).ConfigureAwait(false); try { @@ -53,7 +57,7 @@ public sealed class BleSerialClient : IAsyncDisposable try { await device.RequestMtuAsync(247).ConfigureAwait(false); } catch { /* not supported on this platform */ } - var service = await device.GetServiceAsync(serviceUuid, cancellationToken).ConfigureAwait(false) + var service = await device.GetServiceAsync(serviceUuid, timeoutCts.Token).ConfigureAwait(false) ?? throw new InvalidOperationException($"Service {serviceUuid} not found on {device.Name}."); var notifyChar = await service.GetCharacteristicAsync(notifyUuid).ConfigureAwait(false) @@ -66,12 +70,16 @@ public sealed class BleSerialClient : IAsyncDisposable ? CharacteristicWriteType.WithoutResponse : CharacteristicWriteType.WithResponse; + Console.WriteLine($"BLE: Subscribing to notifications on {notifyUuid}"); notifyChar.ValueUpdated += OnValueUpdated; - await notifyChar.StartUpdatesAsync(cancellationToken).ConfigureAwait(false); + await notifyChar.StartUpdatesAsync(timeoutCts.Token).ConfigureAwait(false); + Console.WriteLine($"BLE: Successfully subscribed to notifications"); _device = device; _notifyChar = notifyChar; _writeChar = writeChar; + + Console.WriteLine($"BLE: Connection complete. Device={device.Name}, CanWrite={writeChar.CanWrite}, CanNotify={notifyChar.Properties.HasFlag(CharacteristicPropertyType.Notify)}"); } catch { @@ -87,7 +95,10 @@ public sealed class BleSerialClient : IAsyncDisposable public async Task WriteAsync(string text, CancellationToken cancellationToken = default) { var writeChar = _writeChar ?? throw new InvalidOperationException("Not connected."); - await writeChar.WriteAsync(Encoding.ASCII.GetBytes(text), cancellationToken).ConfigureAwait(false); + var bytes = Encoding.ASCII.GetBytes(text); + Console.WriteLine($"BLE TX: {text.Replace("\r", "\\r").Replace("\n", "\\n")} ({bytes.Length} bytes)"); + await writeChar.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); + Console.WriteLine("BLE TX: Write completed"); } public async Task DisconnectAsync() @@ -117,13 +128,18 @@ public sealed class BleSerialClient : IAsyncDisposable private void OnValueUpdated(object? sender, CharacteristicUpdatedEventArgs e) { var bytes = e.Characteristic.Value; + Console.WriteLine($"BLE RX: {bytes?.Length ?? 0} bytes"); + if (bytes is null || bytes.Length == 0) return; List lines = new(); lock (_rxBuffer) { - _rxBuffer.Append(Encoding.ASCII.GetString(bytes)); + var text = Encoding.ASCII.GetString(bytes); + Console.WriteLine($"BLE RX decoded: {text.Replace("\r", "\\r").Replace("\n", "\\n")}"); + + _rxBuffer.Append(text); var buffered = _rxBuffer.ToString(); int newline; while ((newline = buffered.IndexOf('\n')) >= 0) @@ -133,12 +149,25 @@ public sealed class BleSerialClient : IAsyncDisposable if (line.Length > 0) lines.Add(line); } + + // If no newlines found but we have data, treat each notification as a complete message + // This handles devices that send complete messages per notification without line endings + if (lines.Count == 0 && buffered.Length > 0) + { + lines.Add(buffered.Trim()); + buffered = ""; + } + _rxBuffer.Clear(); _rxBuffer.Append(buffered); } + Console.WriteLine($"BLE RX: {lines.Count} complete lines"); foreach (var line in lines) + { + Console.WriteLine($"BLE RX line: {line}"); LineReceived?.Invoke(this, line); + } } private void OnDeviceDisconnected(object? sender, DeviceEventArgs e) => HandleDisconnect(e.Device); diff --git a/FieldLogger/Services/DeviceConnectionManager.cs b/FieldLogger/Services/DeviceConnectionManager.cs index d5f4e43..845fbb7 100644 --- a/FieldLogger/Services/DeviceConnectionManager.cs +++ b/FieldLogger/Services/DeviceConnectionManager.cs @@ -126,6 +126,7 @@ public sealed partial class DeviceConnectionManager : ObservableObject { try { + _logger.LogInformation("{Kind} connection attempt {Attempt}", kind, attempt + 1); await BleScanner.EnsurePermissionsAsync(); if (kind == DeviceKind.Locator) await _locator.ConnectAsync(id.Value, cts.Token); @@ -133,19 +134,25 @@ public sealed partial class DeviceConnectionManager : ObservableObject await _gps.ConnectAsync(id.Value, cts.Token); SetState(kind, ConnectionState.Connected); - _logger.LogInformation("{Kind} connected", kind); + _logger.LogInformation("{Kind} connected successfully", kind); return; } catch (OperationCanceledException) { + _logger.LogInformation("{Kind} connection cancelled", kind); return; } catch (Exception ex) { - _logger.LogWarning(ex, "{Kind} connect attempt {Attempt} failed", kind, attempt + 1); + _logger.LogWarning(ex, "{Kind} connect attempt {Attempt} failed: {Message}", kind, attempt + 1, ex.Message); var delay = RetryDelays[Math.Min(attempt, RetryDelays.Length - 1)]; + _logger.LogInformation("{Kind} retrying in {Delay}s...", kind, delay.TotalSeconds); try { await Task.Delay(delay, cts.Token); } - catch (OperationCanceledException) { return; } + catch (OperationCanceledException) + { + _logger.LogInformation("{Kind} retry cancelled", kind); + return; + } } } } diff --git a/FieldLogger/Services/MaglinkService.cs b/FieldLogger/Services/MaglinkService.cs index 47f1c2e..d254be1 100644 --- a/FieldLogger/Services/MaglinkService.cs +++ b/FieldLogger/Services/MaglinkService.cs @@ -12,7 +12,7 @@ public sealed class MaglinkService : IAsyncDisposable { public static readonly Guid ServiceUuid = Guid.Parse("0000fff0-0000-1000-8000-00805f9b34fb"); public static readonly Guid NotifyUuid = Guid.Parse("0000fff2-0000-1000-8000-00805f9b34fb"); - public static readonly Guid WriteUuid = Guid.Parse("0000fff2-0000-1000-8000-00805f9b34fb"); + public static readonly Guid WriteUuid = Guid.Parse("0000fff1-0000-1000-8000-00805f9b34fb"); private readonly BleSerialClient _client = new(); private readonly ILogger _logger; diff --git a/FieldLogger/Services/UmReceiverService.cs b/FieldLogger/Services/UmReceiverService.cs index 681805d..99590c5 100644 --- a/FieldLogger/Services/UmReceiverService.cs +++ b/FieldLogger/Services/UmReceiverService.cs @@ -10,7 +10,7 @@ namespace FieldLogger.Services; /// public sealed class UmReceiverService : IAsyncDisposable { - // UM Receiver BLE External Logging API v1.2: + // UM Receiver BLE External Logging API v1.3: // serial port service with notify (RX) and write-no-response (TX) characteristics. public static readonly Guid ServiceUuid = Guid.Parse("554d0000-261f-677e-a6f1-54c57aa996d4"); public static readonly Guid NotifyUuid = Guid.Parse("554d0001-261f-677e-a6f1-54c57aa996d4"); @@ -23,11 +23,14 @@ public sealed class UmReceiverService : IAsyncDisposable public bool IsConnected => _client.IsConnected; public string? DeviceName => _client.DeviceName; - /// Raised when the operator presses the log button on the receiver. + /// Raised when the operator presses the log button on the receiver or timed logging triggers. public event EventHandler? PacketReceived; public event EventHandler? DeviceInfoReceived; public event EventHandler? Disconnected; + /// Raised for all received lines (for debugging). + public event EventHandler? LineReceived; + public UmReceiverService(ILogger logger) { _logger = logger; @@ -54,12 +57,31 @@ public sealed class UmReceiverService : IAsyncDisposable public Task DisableLoggingAsync(CancellationToken cancellationToken = default) => _client.WriteLineAsync("$UMPBDL,0", cancellationToken); + /// + /// Enables timed data logging at the specified period. + /// + /// Log period multiplier (period = logPeriod * 100ms). Valid: 10-100. + /// Cancellation token. + public Task EnableTimedLoggingAsync(int logPeriod, CancellationToken cancellationToken = default) + { + if (logPeriod < 10 || logPeriod > 100) + throw new ArgumentOutOfRangeException(nameof(logPeriod), "Log period must be between 10 and 100 (1s to 10s)"); + return _client.WriteLineAsync($"$UMETDL,{logPeriod}", cancellationToken); + } + + /// Disables timed data logging. + public Task DisableTimedLoggingAsync(CancellationToken cancellationToken = default) + => _client.WriteLineAsync("$UMETDL,0", cancellationToken); + public Task DisconnectAsync() => _client.DisconnectAsync(); private void OnLineReceived(object? sender, string line) { _logger.LogDebug("UM rx: {Line}", line); + // Notify debug listeners of all lines + LineReceived?.Invoke(this, line); + // Bare command acknowledgements. var trimmed = line.Trim(); if (trimmed.Equals("OK", StringComparison.OrdinalIgnoreCase) || diff --git a/FieldLogger/ViewModels/DebugViewModel.cs b/FieldLogger/ViewModels/DebugViewModel.cs new file mode 100644 index 0000000..e19cbec --- /dev/null +++ b/FieldLogger/ViewModels/DebugViewModel.cs @@ -0,0 +1,254 @@ +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 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; + } + + public void OnAppearing() + { + _receiver.LineReceived += OnLineReceived; + UpdateConnectionStatus(); + } + + public void OnDisappearing() + { + _receiver.LineReceived -= OnLineReceived; + } + + private void UpdateConnectionStatus() + { + IsConnected = _receiver.IsConnected; + if (IsConnected) + { + ConnectionStatus = $"Connected to {_receiver.DeviceName ?? "receiver"}"; + ConnectionStateColor = "Green"; + } + else + { + ConnectionStatus = "Not connected"; + ConnectionStateColor = "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 OnLineReceived(object? sender, string line) + { + MainThread.BeginInvokeOnMainThread(() => + { + var messageType = ClassifyMessage(line); + var parsedData = ParseMessage(line, messageType); + AddMessage("RX", line, messageType, parsedData); + }); + } + + 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}"; + } + } + 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", + "Command" => "Orange", + _ when type.StartsWith("UMLOG") => "Blue", + _ => "Gray" + }; + } + + [RelayCommand] + private void ClearMessages() + { + Messages.Clear(); + MessageCount = 0; + } +} diff --git a/FieldLogger/ViewModels/DeviceScanViewModel.cs b/FieldLogger/ViewModels/DeviceScanViewModel.cs index 64102a8..1333505 100644 --- a/FieldLogger/ViewModels/DeviceScanViewModel.cs +++ b/FieldLogger/ViewModels/DeviceScanViewModel.cs @@ -88,7 +88,7 @@ public sealed partial class DeviceScanViewModel : ObservableObject // Filter by device name prefix based on device type. if (Kind == DeviceKind.Locator && !UmReceiverService.IsUmReceiverName(device.Name)) return; - if (Kind == DeviceKind.Maglink && !MaglinkService.IsMaglinkName(device.Name)) + if (Kind == DeviceKind.RtkGps && !MaglinkService.IsMaglinkName(device.Name)) return; MainThread.BeginInvokeOnMainThread(() => diff --git a/FieldLogger/Views/DebugPage.xaml b/FieldLogger/Views/DebugPage.xaml new file mode 100644 index 0000000..ae24bde --- /dev/null +++ b/FieldLogger/Views/DebugPage.xaml @@ -0,0 +1,91 @@ + + + + + +