using FieldLogger.Models; using FieldLogger.Services.Ble; using Microsoft.Extensions.Logging; namespace FieldLogger.Services; /// /// Manages the connection to an Underground Magnetics locating receiver and /// its push-button data-logging (PBDL) protocol. /// 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 _logger; public UmDeviceInfo? DeviceInfo { get; private set; } public bool IsConnected => _client.IsConnected; public string? DeviceName => _client.DeviceName; /// 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; _client.LineReceived += OnLineReceived; _client.Disconnected += (_, _) => Disconnected?.Invoke(this, EventArgs.Empty); } /// Device-name filter for scan results (UMRX_* for most brands, DT100_* for Leica). 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); } /// Enables push-button data logging. The first enable triggers the info string. public Task EnableLoggingAsync(CancellationToken cancellationToken = default) => _client.WriteLineAsync("$UMPBDL,1", cancellationToken); 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) || 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(); }