Files
ulapp/FieldLogger/Services/UmReceiverService.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

111 lines
4.5 KiB
C#

using FieldLogger.Models;
using FieldLogger.Services.Ble;
using Microsoft.Extensions.Logging;
namespace FieldLogger.Services;
/// <summary>
/// Manages the connection to an Underground Magnetics locating receiver and
/// its push-button data-logging (PBDL) protocol.
/// </summary>
public sealed class UmReceiverService : IAsyncDisposable
{
// 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");
public static readonly Guid WriteUuid = Guid.Parse("554d0002-261f-677e-a6f1-54c57aa996d4");
private readonly BleSerialClient _client = new();
private readonly ILogger<UmReceiverService> _logger;
public UmDeviceInfo? DeviceInfo { get; private set; }
public bool IsConnected => _client.IsConnected;
public string? DeviceName => _client.DeviceName;
/// <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;
_client.LineReceived += OnLineReceived;
_client.Disconnected += (_, _) => Disconnected?.Invoke(this, EventArgs.Empty);
}
/// <summary>Device-name filter for scan results (UMRX_* for most brands, DT100_* for Leica).</summary>
public static bool IsUmReceiverName(string? name) =>
name is not null &&
(name.StartsWith("UMRX", StringComparison.OrdinalIgnoreCase) ||
name.StartsWith("DT100", StringComparison.OrdinalIgnoreCase));
public async Task ConnectAsync(Guid deviceId, CancellationToken cancellationToken = default)
{
await _client.ConnectAsync(deviceId, ServiceUuid, NotifyUuid, WriteUuid, cancellationToken);
await EnableLoggingAsync(cancellationToken);
}
/// <summary>Enables push-button data logging. The first enable triggers the info string.</summary>
public Task EnableLoggingAsync(CancellationToken cancellationToken = default)
=> _client.WriteLineAsync("$UMPBDL,1", cancellationToken);
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) ||
trimmed.Equals("ERROR", StringComparison.OrdinalIgnoreCase))
return;
var packet = UmLogPacket.TryParse(line);
if (packet is not null)
{
PacketReceived?.Invoke(this, packet);
return;
}
var info = UmDeviceInfo.TryParse(line);
if (info is not null)
{
DeviceInfo = info;
DeviceInfoReceived?.Invoke(this, info);
return;
}
_logger.LogWarning("UM receiver sent unrecognized line: {Line}", line);
}
public ValueTask DisposeAsync() => _client.DisposeAsync();
}