ios.net-corexamarin.iosios-bluetooth

Xamarin iOS Bluetooth peripheral scanning never sees any peripherals


I am trying to create a Xamarin.Forms app that will run on both iOS and Android. Eventually I need instances of the app to communicate with each other via Bluetooth, but I'm stuck on getting the iOS side to do anything with Bluetooth. I originally tried to work with Plugin.BluetoothLE and Plugin.BLE, but after a week and a half I was not able to get advertising or scanning to work on either OS with either plugin, so I decided to try implementing simple Bluetooth interaction using the .NET wrappers of the platform APIs, which at least are well documented. I did get scanning to work fine on the Android side. With iOS, though, what I have right now builds just fine, and runs on my iPad without errors, but the DiscoveredPeripheral handler is never called, even though the iPad is just a few inches from the Android tablet and presumably should be able to see the same devices. I have verified this by setting a breakpoint in that method, which is never reached; and when I open the Bluetooth Settings on the iPad to make it discoverable the app version on the Android tablet can see it, so I don't think it's an iPad hardware issue.

It seems obvious that there is simply some part of the process I don't know to do, but it's not obvious (to me) where else to look to find out what it is. Here is the code for the class that interacts with the CBCentralManager (as far as I understand from what I've read, this should include everything necessary to return a list of peripherals):

using MyBluetoothApp.Shared; // for the interfaces and constants
using CoreBluetooth;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xamarin.Forms;

[assembly: Dependency(typeof(MyBluetoothApp.iOS.PeripheralScanner))]
namespace MyBluetoothApp.iOS
{
    public class PeripheralScanner : IPeripheralScanner
    {
        private readonly CBCentralManager manager;
        private List<IPeripheral> foundPeripherals;

        public PeripheralScanner()
        {
            this.foundPeripherals = new List<IPeripheral>();

            this.manager = new CBCentralManager();
            this.manager.DiscoveredPeripheral += this.DiscoveredPeripheral;
            this.manager.UpdatedState += this.UpdatedState;
        }

        public async Task<List<IPeripheral>> ScanForService(string serviceUuid)
        {
            return await this.ScanForService(serviceUuid, BluetoothConstants.DEFAULT_SCAN_TIMEOUT);
        }

        public async Task<List<IPeripheral>> ScanForService(string serviceUuid, int duration)
        {
            CBUUID uuid = CBUUID.FromString(serviceUuid);
            //this.manager.ScanForPeripherals(uuid);
            this.manager.ScanForPeripherals((CBUUID)null); // For now I'd be happy to see ANY peripherals

            await Task.Delay(duration);
            this.manager.StopScan();

            return this.foundPeripherals;
        }

        private void DiscoveredPeripheral(object sender, CBDiscoveredPeripheralEventArgs args)
        {
            this.foundPeripherals.Add(new CPeripheral(args.Peripheral));
        }

        private void UpdatedState(object sender, EventArgs args)
        {
            CBCentralManagerState state = ((CBCentralManager)sender).State;
            if (CBCentralManagerState.PoweredOn != state)
            {
                throw new Exception(state.ToString());
            }
        }
    }
}

Can anyone point me in the direction of understanding what I'm missing?

EDIT: O...K, I've discovered quite by accident that if I do this in the shared code:

IPeripheralScanner scanner = DependencyService.Get<IPeripheralScanner>();
List<IPeripheral> foundPeripherals = await scanner.ScanForService(BluetoothConstants.VITL_SERVICE_UUID);

twice in a row, it works the second time. I feel both more hopeful and much more confused.


Solution

  • The underlying problem was that in the first instantiation of PeripheralScanner, ScanForService was being called before State was updated. I tried many ways of waiting for that event to be raised so I could be sure the state was PoweredOn, but nothing seemed to work; polling loops simply never reached the desired state, but if I threw an Exception in the UpdatedState handler it was thrown within milliseconds of launch and the state at that time was always PoweredOn. (Breakpoints in that handler caused the debugging to freeze with the output Resolved pending breakpoint, which not even the VS team seems to be able to explain).

    Reading some of the Apple developer blogs I found that this situation is most often avoided by having the desired action occur within the UpdatedState handler. It finally soaked into my thick head that I was never seeing any effects from that handler running because the event was being raised and handled on a different thread. I really need to pass the service UUID to the scanning logic, and to interact with a generic List that I can return from ScanForService, so just moving it all to the handler didn't seem like a promising direction. So I created a singleton for flagging the state:

    internal sealed class ManagerState // .NET makes singletons easy - Lazy<T> FTW
    {
        private static readonly Lazy<ManagerState> lazy = new Lazy<ManagerState>(() => new ManagerState());
        internal static ManagerState Instance { get { return ManagerState.lazy.Value; } }
        internal bool IsPoweredOn { get; set; }
    
        private ManagerState()
        {
            this.IsPoweredOn = false;
        }
    }
    

    and update it in the handler:

    private void updatedState(object sender, EventArgs args)
    {
        ManagerState.Instance.IsPoweredOn = CBCentralManagerState.PoweredOn == ((CBCentralManager) sender).State;
    }
    

    then poll that at the beginning of ScanForService (in a separate thread each time because, again, I will not see the updates in my base thread):

        while (false == await Task.Run(() => ManagerState.Instance.IsPoweredOn)) { }
    

    I'm not at all sure this is the best solution, but it does work, at least in my case. I guess I could move the logic to the handler and create a fancier singleton class for moving all the state back and forth, but that doesn't feel as good to me.