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:
@@ -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<string> 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MaglinkService> _logger;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace FieldLogger.Services;
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Raised when the operator presses the log button on the receiver.</summary>
|
||||
/// <summary>Raised when the operator presses the log button on the receiver or timed logging triggers.</summary>
|
||||
public event EventHandler<UmLogPacket>? PacketReceived;
|
||||
public event EventHandler<UmDeviceInfo>? DeviceInfoReceived;
|
||||
public event EventHandler? Disconnected;
|
||||
|
||||
/// <summary>Raised for all received lines (for debugging).</summary>
|
||||
public event EventHandler<string>? LineReceived;
|
||||
|
||||
public UmReceiverService(ILogger<UmReceiverService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -54,12 +57,31 @@ public sealed class UmReceiverService : IAsyncDisposable
|
||||
public Task DisableLoggingAsync(CancellationToken cancellationToken = default)
|
||||
=> _client.WriteLineAsync("$UMPBDL,0", cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enables timed data logging at the specified period.
|
||||
/// </summary>
|
||||
/// <param name="logPeriod">Log period multiplier (period = logPeriod * 100ms). Valid: 10-100.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Disables timed data logging.</summary>
|
||||
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) ||
|
||||
|
||||
Reference in New Issue
Block a user