跨平台移动应用可以做广告,可以扫描,不能通过蓝牙 API 连接

问题描述

我正在开发一个 Xamarin.Forms(版本 4.8.0.1687)跨平台应用程序,该应用程序需要能够使用蓝牙 LE 与其自身的其他实例通信。我对蓝牙采用共享接口/平台特定的实现方法,因为我们计划最终扩展支持的平台,以包括一些并非所有知名软件包都支持的平台。我现在使用的版本实际上只是一个概念验证,我还没有进行广泛的错误检查或边缘情况的处理,因此代码仍然相当简单。

我已经能够在 Android 和 iOS 平台上做广告,并通过服务 UUID 过滤扫描查看来自两个平台的其他正在做广告的应用实例。但我无法从两者建立连接。在 Android 上,始终如一地调用 BluetoothSocket.Connect() 会导致抛出 Java.IO.IOException 消息读取失败,套接字可能已关闭或超时,读取 ret: -1 > 经过几秒钟的延迟。在 iOS 上,调用 CBCentralManager.ConnectPeripheral(CBPeripheral) 永远不会引发 ConnectedPeripheral 事件,永远不会引发 FailedToConnectPeripheral 事件,也永远不会引发异常。如果我在等待外围设备的状态改变的循环中调用它,那么循环只会无限期地运行。

我在两个平台上的同一个地方都遇到了障碍的事实向我表明,我误解了蓝牙过程而不是代码本身,这并不令人惊讶,因为这是我第一次已在任何平台上完成任何蓝牙 API 编程。但我只是猜测。如果有人能告诉我我在这两个 API 上哪里出错了,我会衷心感谢您,再加上 4 美元,您将获得一杯像样的咖啡。

Android 代码

执行广告(有效)的 Android 类:

    public class Advertiser : IAdvertiser
    {
        private AdvertiserCallback callback; // extends AdvertiseCallback

        public void Advertise(string serviceUuid,string serviceName)
        {
            AdvertiseSettings settings = new AdvertiseSettings.Builder().SetConnectable(true).Build();

            BluetoothAdapter.DefaultAdapter.SetName(serviceName);
            ParcelUuid parcelUuid = new ParcelUuid(UUID.FromString(serviceUuid));
            AdvertiseData data = new AdvertiseData.Builder().AddServiceUuid(parcelUuid).SetIncludeDeviceName(true).Build();

            this.callback = new AdvertiserCallback();
            BluetoothAdapter.DefaultAdapter.BluetoothLeAdvertiser.StartAdvertising(settings,data,callback);

            BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor
            (
                UUID.FromString(BluetoothConstants.DESCRIPTOR_UUID),GattDescriptorPermission.Read | GattDescriptorPermission.Write
            );
            BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic
            (
                UUID.FromString(BluetoothConstants.CHaraCTERISTIC_UUID),GattProperty.Read | GattProperty.Write,GattPermission.Read | GattPermission.Write
            );
            characteristic.AddDescriptor(descriptor);
            BluetoothGattService service = new BluetoothGattService
            (
                UUID.FromString(BluetoothConstants.SERVICE_UUID),GattServiceType.Primary
            );
            service.AddCharacteristic(characteristic);

            BluetoothManager manager = (BluetoothManager)Android.App.Application.Context.GetSystemService(Context.BluetoothService);
            BluetoothGattServer server = manager.OpenGattServer(Android.App.Application.Context,new GattServerCallback());
            server.AddService(service);
        }

        public void StopAdvertising()
        {
            if (null != this.callback)
            {
                BluetoothAdapter.DefaultAdapter.BluetoothLeAdvertiser.StopAdvertising(this.callback);
            }
        }
    }

执行扫描(有效)的 Android 类:

    public class DeviceScanner : Iperipheralscanner
    {
        public async Task<List<IPeripheral>> ScanForService(string serviceUuid) // IPeripheral wraps CBPeripheral or BluetoothDevice
        {
            return await this.ScanForService(serviceUuid,BluetoothConstants.DEFAULT_SCAN_TIMEOUT);
        }

        public async Task<List<IPeripheral>> ScanForService(string serviceUuid,int duration)
        {
            List<ScanFilter> filters = new List<ScanFilter>();
            filters.Add(new ScanFilter.Builder().SetServiceUuid(new ParcelUuid(UUID.FromString(BluetoothConstants.SERVICE_UUID))).Build());

            ScannerCallback callback = new ScannerCallback(); // extends ScanCallback

            BluetoothLeScanner scanner = BluetoothAdapter.DefaultAdapter.BluetoothLeScanner;

            Devices.Instance.DeviceList.Clear(); // Devices is a singleton for passing a List of found Iperipherals across threads

            scanner.StartScan(filters,new ScanSettings.Builder().Build(),callback);
            await Task.Delay(duration);
            scanner.StopScan(callback);

            return Devices.Instance.DeviceList;
        }
    }

执行连接的 Android 类(从不工作):

    public class DeviceConnector : IPeripheralConnector
    {
        public void Connect(IPeripheral peripheral)
        {
            BluetoothDevice device = (BluetoothDevice)peripheral.Peripheral;
            if (device.BondState != Bond.Bonded)
            {
                device.CreateBond();
            }

            BluetoothAdapter.DefaultAdapter.Canceldiscovery();
            BluetoothSocket socket;
            try
            {
                socket = device.CreateRfcommSocketToServiceRecord(UUID.FromString(BluetoothConstants.SERVICE_UUID));
                if (null == socket)
                {
                    throw new System.Exception("Failed to create socket");
                }
                socket.Connect(); // IOException is consistently thrown here after several seconds
            }
            catch (IOException x)
            {
                Method method = device.Class.getmethod("createRfcommSocket",Integer.Type);
                socket = (BluetoothSocket)method.Invoke(device,1);
                if (null == socket)
                {
                    throw new System.Exception("Failed to create socket");
                }
                socket.Connect(); // IOException is consistently thrown here after several seconds
            }
            if (false == socket.IsConnected)
            {
                throw new System.Exception(string.Format("Failed to connect to service {0}",device.Name));
            }
        }
    }

iOS 代码

执行广告(有效)的 iOS 类:

    public class Advertiser : IAdvertiser
    {
        private readonly CBPeripheralManager manager;

        public Advertiser()
        {
            this.manager = new CBPeripheralManager();
            this.manager.StateUpdated += this.StateUpdated;
        }

        public async void Advertise(string serviceUuid,string serviceName)
        {
            // The state needs to be polled in separate threads because the update event is being raised
            // and handled in a different thread than this code; we'll never see the update directly
            // PeripheralManagerState is a singleton for tracking the state between classes and threads
            while (false == await Task.Run(() => PeripheralManagerState.Instance.IsPoweredOn)) { }

            CBUUID svcUuid = CBUUID.FromString(serviceUuid);

            CBMutableCharacteristic characteristic = new CBMutableCharacteristic
            (
                CBUUID.FromString(BluetoothConstants.CHaraCTERISTIC_UUID),CBCharacteristicProperties.Read | CBCharacteristicProperties.Write,null,CBAttributePermissions.Readable | CBAttributePermissions.Writeable
            );
            CBMutableService vitlService = new CBMutableService(svcUuid,true);
            vitlService.characteristics = new[] { characteristic };

            StartAdvertisingOptions options = new StartAdvertisingOptions();
            options.ServicesUUID = new[] { svcUuid };
            options.LocalName = serviceName;

            this.manager.AddService(vitlService);
            this.manager.StartAdvertising(options);
        }

        public void StopAdvertising()
        {
            if (this.manager.Advertising)
            {
                this.manager.StopAdvertising();
            }
        }

        internal void StateUpdated(object sender,EventArgs args)
        {
            CBPeripheralManagerState state = ((CBPeripheralManager)sender).State;
            if (CBPeripheralManagerState.PoweredOn == state)
            {
                PeripheralManagerState.Instance.IsPoweredOn = true;
            }
            else
            {
                throw new Exception(state.ToString());
            }
        }
    }

执行扫描(有效)的 iOS 类:

    public class peripheralscanner : Iperipheralscanner
    {
        private readonly CBCentralManager manager;
        private List<IPeripheral> foundperipherals; // IPeripheral wraps CBPeripheral or BluetoothDevice

        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,int duration)
        {
            // The state needs to be polled in separate threads because the update event is being raised
            // and handled in a different thread than this code; we'll never see the update directly
            // CentralManagerState is a singleton for tracking the state between classes and threads
            while (false == await Task.Run(() => CentralManagerState.Instance.IsPoweredOn)) { }

            if (this.manager.IsScanning)
            {
                this.manager.StopScan();
            }
            this.manager.ScanForperipherals(CBUUID.FromString(serviceUuid));
            await Task.Delay(duration);
            this.manager.StopScan();

            return this.foundperipherals;
        }

        private void discoveredPeripheral(object sender,CBdiscoveredPeripheralEventArgs args)
        {
            CBPeripheral cbperipheral = args.Peripheral;
            bool isdiscovered = false;
            foreach (IPeripheral peripheral in this.foundperipherals)
            {
                if (((CBPeripheral)peripheral.Peripheral).Identifier == cbperipheral.Identifier)
                {
                    isdiscovered = true;
                    break;
                }
            }
            if (false == isdiscovered)
            {
                this.foundperipherals.Add(new CPeripheral(cbperipheral));
            }
        }

        private void updatedState(object sender,EventArgs args)
        {
            CBCentralManagerState state = ((CBCentralManager)sender).State;
            if (CBCentralManagerState.PoweredOn == state)
            {
                CentralManagerState.Instance.IsPoweredOn = true;
            }
            else
            {
                throw new Exception(state.ToString());
            }
        }
    }

执行连接的 iOS 类(从不工作):

    public class PeripheralConnector : IPeripheralConnector
    {
        private readonly CBCentralManager manager;
        private CBPeripheral peripheral;

        public PeripheralConnector()
        {
            this.manager = new CBCentralManager();
            this.manager.ConnectedPeripheral += this.connectedPeripheral;
            this.manager.FailedToConnectPeripheral += this.FailedToConnectPeripheral;
        }

        public void Connect(IPeripheral peripheral)
        {
            this.peripheral = (CBPeripheral)peripheral.Peripheral;
            while (CBperipheralstate.Connected != this.peripheral.State) // this loop runs until I kill the app
            {
                this.manager.ConnectPeripheral(this.peripheral);
            }
        }

        private void connectedPeripheral(object sender,CBPeripheralEventArgs args)
        {
            throw new Exception(args.Peripheral.Name);
        }

        private void FailedToConnectPeripheral(object sender,CBPeripheralErrorEventArgs args)
        {
            throw new Exception(args.Error.LocalizedFailureReason);
        }
    }

编辑:至少我已经能够连接 Android 应用程序。我显然使用了不正确的类;我发现的大多数连接示例似乎都指的是经典蓝牙。至少对我来说,这是有效的(显然它在前进之前需要进行错误检查):

public class DeviceConnector : IPeripheralConnector
{
    private GattCallback callback; // GattCallback inherits from BluetoothGattCallback
    private bool isConnected = false;
    public bool IsConnected { get { return this.isConnected; } }

    public void Connect(IPeripheral peripheral)
    {
        BluetoothDevice device = (BluetoothDevice)peripheral.Peripheral;
        if (device.BondState != Bond.Bonded)
        {
            device.CreateBond();
        }

        BluetoothAdapter.DefaultAdapter.Canceldiscovery();
        this.callback = new GattCallback();
        device.ConnectGatt(Android.App.Application.Context,true,this.callback);
        while (false == this.callback.IsConnected) { }
        this.isConnected = this.callback.IsConnected;
    }
}

我仍然无法让 iOS 应用连接到 Android 应用或 iOS 应用的另一个实例。我发现的每个声称是工作示例的示例都使用与我所做的完全相同的调用,因此继续调查和挠头。

解决方法

如上面的编辑中所述,Android 案例中的解决方案是在 BluetoothDevice 对象上调用 ConnectGatt()

通过 Apple 文档和教程工作数天后,我意识到我做错的是保留代表找到的外围设备的 CBPeripheral 实例,但不一定是发现的 CBCentralManager 实例它。在 iOS 中,您必须在扫描并发现外围设备的 CBCentralManager 的同一实例上调用 ConnectPeripheral() 。我以允许保留实例的方式重构了共享项目,现在我可以从任一操作系统连接到任一操作系统。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...