Alex Vokh
Alex Vokh

Reputation: 1

Continuous Detection of Readers in the Background and on Locked Screens on iOS Using .NET MAUI

I am developing a .NET MAUI application that uses iBeacon to unlock a door when the user approaches the E104-BT52 reader. I have both iBeacon and BLE capabilities. I want the application to detect when the reader is nearby, even when the phone is locked or when the app is not active, and send data to the reader.

Issues with iBeacon: According to the documentation, StartRangingBeacons works only when the app is active, while StartMonitoring should still function even when the app is in the background or the iPhone is locked. However, I am facing an issue: RegionEntered and RegionLeft events are not being triggered as expected when the app is in the background or when the phone is locked.

Issues with CBCentralManager: When using CBCentralManager, I receive data in the background approximately every 20 seconds, but it does not work when the phone is locked.

If I attempt to run both iBeacon and BLE scanning simultaneously, the detection works as expected in both the background and on a locked screen. However, both methods stop functioning if used separately. This approach also requires keeping StartRangingBeacons constantly active, which is not acceptable due to its high energy consumption.

How can I ensure continuous and reliable detection of the reader in the background and on a locked screen on iOS?

What approaches or recommendations exist to ensure reliable background scanning on iOS to meet these requirements?

Thanks

My current approach IBeacon:

using CoreLocation;
using Foundation;

public class BeaconManager : CLLocationManagerDelegate
{
    protected static BeaconManager _instance;
    protected static readonly object _lock = new object();

    protected readonly CLLocationManager _locationManager;
    protected CLBeaconRegion _beaconRegion;

    protected static bool _isScanning;

    protected static readonly NSUuid BeaconUuid = new NSUuid("0000FFF0-454E-5445-5243-414D2D424C45");
    protected const string BeaconRegionIdentifier = "com.entercam.pass.beacon";

    public BeaconManager()
    {
        _locationManager = new CLLocationManager
        {
            Delegate = this,
            AllowsBackgroundLocationUpdates = true,
            PausesLocationUpdatesAutomatically = false,
        };

        if (CLLocationManager.Status != CLAuthorizationStatus.AuthorizedAlways)
        {
            _locationManager.RequestAlwaysAuthorization();
        }

        _beaconRegion = new CLBeaconRegion(BeaconUuid, BeaconRegionIdentifier)
        {
            NotifyEntryStateOnDisplay = true,
            NotifyOnEntry = true,
            NotifyOnExit = true
        };
    }

    public static BeaconManager Instance
    {
        get
        {
            lock (_lock)
            {
                return _instance ??= new BeaconManager();
            }
        }
    }

    public void CheckAndUpdateServiceState()
    {
        if (CLLocationManager.Status == CLAuthorizationStatus.AuthorizedAlways)
        {
            StartScanning();
        }
        else
        {
            StopScanning();
        }
    }

    private void StartScanning()
    {
        if (_isScanning) return;

        _isScanning = true;

        _locationManager.StartMonitoring(_beaconRegion);

        Console.WriteLine("Started scanning for beacons.");
    }


    private void StopScanning()
    {
        if (_isScanning == false) return;

        _isScanning = false;

        _locationManager.StopMonitoring(_beaconRegion);

        Console.WriteLine("Stopped scanning for beacons.");
    }

    public override void DidDetermineState(CLLocationManager manager, CLRegionState state, CLRegion region)
    {
        Console.WriteLine($"State: {state} for region {region.Identifier}");

        if (state == CLRegionState.Inside && region is CLBeaconRegion)
        {
            _locationManager.StartMonitoring(region as CLBeaconRegion);

            Console.WriteLine("Started ranging beacons inside region.");
        }
        else if (state == CLRegionState.Outside && region is CLBeaconRegion)
        {
            _locationManager.StopMonitoring(region as CLBeaconRegion);

            Console.WriteLine("Stopped ranging beacons outside region.");
        }
    }

    public override void RegionEntered(CLLocationManager manager, CLRegion region)
    {
        Console.WriteLine($"Entered region: {region.Identifier}");
        if (region is CLBeaconRegion beaconRegion)
        {
            _locationManager.StartRangingBeacons(beaconRegion);

            Console.WriteLine("Started ranging beacons.");
        }
    }

    public override void RegionLeft(CLLocationManager manager, CLRegion region)
    {
        Console.WriteLine($"Left region: {region.Identifier}");
        if (region is CLBeaconRegion beaconRegion)
        {
            _locationManager.StopRangingBeacons(beaconRegion);

            Console.WriteLine("Stopped ranging beacons.");
        }
    }

    public override void DidRangeBeacons(CLLocationManager manager, CLBeacon[] beacons, CLBeaconRegion region)
    {
        if (beacons.Length > 0)
        {
            foreach (var beacon in beacons)
            {
                Console.WriteLine($"Beacon detected: UUID={beacon.ProximityUuid}, Major={beacon.Major}, Minor={beacon.Minor}, RSSI={beacon.Rssi}");
            }
        }
        else
        {
            Console.WriteLine("No beacons detected.");
        }
    }
}

Info.plist:

<key>UIBackgroundModes</key>
    <array>
      <string>location</string>
      <string>bluetooth-central</string>
      <string>bluetooth-peripheral</string>
    </array>
        
<key>NSLocationWhenInUseUsageDescription</key>
<key>NSLocationAlwaysUsageDescription</key>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<key>NSBluetoothAlwaysUsageDescription</key>
<key>NSBluetoothPeripheralUsageDescription</key><key>NSUserNotificationUsageDescription</key>

MauiProgram:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
#if ANDROID
        builder.Services.AddSingleton<Platforms.Android.Services.BleForegroundService>();
#endif

#if IOS
        // IBeacon
     builder.Services.AddSingleton<Platforms.iOS.Services.BeaconManager>(Platforms.iOS.Services.BeaconManager.Instance);
        
        // CBCentralManager
      builder.Services.AddSingleton<Platforms.iOS.Services.BleCentralManager>(Platforms.iOS.Services.BleCentralManager.Instance);
#endif
       
        MauiApp mauiApp = builder.Build();

        return mauiApp;
    }
}

My current approach CBCentralManager:

using CoreBluetooth;
using CoreFoundation;
using EntercamPass.Services;
using Foundation;

public class BleCentralManager : CBCentralManagerDelegate
{
    private static BleCentralManager _instance;
    private static readonly object _lock = new object();

    private CBCentralManager _centralManager;

    private static bool _isScanning;

    private BleCentralManager()
    {
        _centralManager = new CBCentralManager(this, new DispatchQueue("BleCentralManagerQueue"), new NSDictionary(
            CBCentralManager.OptionRestoreIdentifierKey, new NSString("bla.bla")
        ));
    }

    public static BleCentralManager Instance
    {
        get
        {
            lock (_lock)
            {
                return _instance ??= new BleCentralManager();
            }
        }
    }

    public void CheckAndUpdateServiceState()
    {
        if (_centralManager.State == CBManagerState.PoweredOn)
        {
            StartScanning();
        }
        else
        {
            StopScanning();
        }
    }

    private void StartScanning()
    {
        if (_isScanning) return;

        _isScanning = true;

        _centralManager.ScanForPeripherals(
            new[] { CBUUID.FromString(BleService.BLE_SERVICE_UID.ToString()) },
            new NSDictionary(CBCentralManager.ScanOptionAllowDuplicatesKey, NSNumber.FromBoolean(true))
        );

        Console.WriteLine("Started scanning");
    }

    private void StopScanning()
    {
        if (_isScanning == false) return;

        _isScanning = false;

        _centralManager.StopScan();
        
        Console.WriteLine("Stopped scanning for peripherals.");
    }

    public override void UpdatedState(CBCentralManager central)
    {
        Console.WriteLine($"Bluetooth state updated: {central.State}");

        CheckAndUpdateServiceState();
    }

    public override void WillRestoreState(CBCentralManager central, NSDictionary dict)
    {
        Console.WriteLine("Restoring state...");

        if (dict.ContainsKey(CBCentralManager.RestoredStatePeripheralsKey))
        {
            var peripherals = dict[CBCentralManager.RestoredStatePeripheralsKey] as NSArray;
            foreach (CBPeripheral peripheral in NSArray.FromArray<CBPeripheral>(peripherals))
            {
                Console.WriteLine($"Restored connection to {peripheral.Name}");

                _centralManager.ConnectPeripheral(peripheral);
            }
        }

        CheckAndUpdateServiceState();
    }

    public override void DiscoveredPeripheral(CBCentralManager central, CBPeripheral peripheral, NSDictionary advertisementData, NSNumber rssi)
    {
        string deviceName = peripheral.Name ?? "Unknown Device";
        string deviceId = peripheral.Identifier.ToString();

        int rssiValue = rssi.Int32Value;

        var manufacturerData = advertisementData[CBAdvertisement.DataManufacturerDataKey] as NSData;
        var serviceUuids = advertisementData[CBAdvertisement.DataServiceUUIDsKey] as NSArray;

        string serviceUuidsString = serviceUuids != null
            ? string.Join(", ", serviceUuids.Select(uuid => uuid.ToString()))
            : "No Services";

        Console.WriteLine($"Discovered Peripheral: Name: {deviceName}, ID: {deviceId}, RSSI: {rssiValue}");
        Console.WriteLine($"Manufacturer Data: {manufacturerData?.ToArray() ?? new byte[0]}, Services: {serviceUuidsString}");

        // Handle discovered devices
        // BleService.Instance.OnDeviceDiscovered(peripheral, rssi);

        _centralManager.ConnectPeripheral(peripheral);
    }

    public override void ConnectedPeripheral(CBCentralManager central, CBPeripheral peripheral)
    {
        Console.WriteLine($"Connecting to {peripheral.Name ?? "Unknown Device"}");

        // Discover services on the connected peripheral
        peripheral.DiscoverServices();
    }

    public void ConnectPeripheral(CBPeripheral peripheral)
    {
        var options = new NSDictionary(
            new NSString("CBConnectPeripheralOptionNotifyOnConnection"), NSNumber.FromBoolean(true),
            new NSString("CBConnectPeripheralOptionNotifyOnDisconnection"), NSNumber.FromBoolean(true),
            new NSString("CBConnectPeripheralOptionNotifyOnNotification"), NSNumber.FromBoolean(true)
        );

        _centralManager.ConnectPeripheral(peripheral, options);
        Console.WriteLine($"Connecting to {peripheral.Name ?? "Unknown Device"}");
    }
}

Upvotes: 0

Views: 74

Answers (0)

Related Questions