Files
ulapp/FieldLogger/ViewModels/DebugViewModel.cs
brentperteet 299ec3875d Add Debug page and fix BLE communication issues
Major changes:
- Added Debug tab with real-time message logging and hex viewer
- Implemented timed data logging ($UMTDL command) per API v1.3
- Fixed BLE message parsing to handle notifications without line endings
- Added Schema 2 support (22-field timed logging packets)
- Updated Maglink BLE UUIDs (fff0/fff1/fff2)
- Added connection timeout (10s) to prevent hanging on unavailable devices
- Added connection status display and manual push-button logging trigger
- Improved retry logging with attempt numbers and delays
- Added console window allocation for Windows debug output
- Added BLE disconnect on app close

Bug fixes:
- Fixed device name filtering for Maglink (ML-* prefix)
- Fixed RtkGps enum reference in DeviceScanViewModel
- Fixed DebugMessage namespace (moved to Models)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 17:32:00 -05:00

255 lines
6.9 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 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;
}
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;
}
}