Initial commit: FieldLogger MAUI app with Maglink BLE support

Added Maglink RTK GNSS receiver integration with correct BLE UUIDs and device name filtering (ML-* prefix).

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
brentperteet
2026-07-06 13:46:50 -05:00
commit b602b762c9
79 changed files with 5807 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;
namespace FieldLogger.Services.Ble;
/// <summary>A device found during a BLE scan.</summary>
public sealed record DiscoveredDevice(Guid Id, string Name, int Rssi);
/// <summary>Thin scan wrapper over Plugin.BLE with per-platform permission handling.</summary>
public sealed class BleScanner
{
private readonly IAdapter _adapter = CrossBluetoothLE.Current.Adapter;
public event EventHandler<DiscoveredDevice>? DeviceDiscovered;
public bool IsScanning => _adapter.IsScanning;
/// <summary>
/// Scans for BLE devices for the given duration, raising DeviceDiscovered as devices appear.
/// Only named devices are reported.
/// </summary>
public async Task ScanAsync(TimeSpan duration, CancellationToken cancellationToken = default)
{
await EnsurePermissionsAsync();
if (!CrossBluetoothLE.Current.IsOn)
throw new InvalidOperationException("Bluetooth is turned off.");
void OnDiscovered(object? sender, DeviceEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.Device.Name))
DeviceDiscovered?.Invoke(this, new DiscoveredDevice(e.Device.Id, e.Device.Name, e.Device.Rssi));
}
_adapter.DeviceDiscovered += OnDiscovered;
_adapter.DeviceAdvertised += OnDiscovered;
try
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(duration);
_adapter.ScanTimeout = (int)duration.TotalMilliseconds;
try
{
await _adapter.StartScanningForDevicesAsync(cancellationToken: timeout.Token);
}
catch (OperationCanceledException)
{
// Scan window elapsed or caller cancelled - both are normal.
}
}
finally
{
_adapter.DeviceDiscovered -= OnDiscovered;
_adapter.DeviceAdvertised -= OnDiscovered;
if (_adapter.IsScanning)
await _adapter.StopScanningForDevicesAsync();
}
}
/// <summary>Requests the runtime permissions BLE scanning needs (Android only; no-op elsewhere).</summary>
public static async Task EnsurePermissionsAsync()
{
#if ANDROID
if (OperatingSystem.IsAndroidVersionAtLeast(31))
{
var status = await Permissions.RequestAsync<Permissions.Bluetooth>();
if (status != PermissionStatus.Granted)
throw new PermissionException("Bluetooth permission was denied.");
}
else
{
var status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
throw new PermissionException("Location permission (required for BLE scanning) was denied.");
}
#else
await Task.CompletedTask;
#endif
}
}