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>
This commit is contained in:
254
FieldLogger/ViewModels/DebugViewModel.cs
Normal file
254
FieldLogger/ViewModels/DebugViewModel.cs
Normal file
@@ -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<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;
|
||||
}
|
||||
}
|
||||
@@ -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(() =>
|
||||
|
||||
Reference in New Issue
Block a user