xamarinxamarin.formsbluetoothlescannerbluetoothleadvertiserplugin.bluetoothle

Sending data between two phones with bluetooth (I'm trying to use aritchie's Plugin.BluetoothLE plugin)


I'm trying to send data between two phones (e.g. two iPhones, or maybe even cross platform) via bluetooth.

I've been trying to use Plugin.BluetoothLE from NuGet, which appears to have been updated recently (March 2020), however I can't seem to get any of the sample code working (details below).

I'd be grateful if anyone can point out what's wrong below, and/or if there's a better way of sending data between two phones through bluetooth. My application is time dependent, and there may not be a wifi network, so bluetooth seems to be the best option...

When I implement the demo server code available at https://github.com/aritchie/bluetoothle, I get the following errors:

No 'AddService' method within CrossBleAdapter.Current.CreateGattServer().

No 'Start' method within CrossBleAdapter.Current.CreateGattServer().

Here's the code I'm using (which I'm calling from the form).

using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Plugin.BluetoothLE;
using Plugin.BluetoothLE.Server;

namespace BluetoothTest.Models
{
    public class BluetoothServer
    {

        public BluetoothServer()
        {

        }

        public void StartAdvertising()
        {
            //Guid[] guidArray = new Guid[1];
            List<Guid> guidArray;
            guidArray = new List<Guid>();
            guidArray.Add(Guid.NewGuid());

            CrossBleAdapter.Current.Advertiser.Start(new AdvertisementData
            {
                LocalName = "TestServer",
                ServiceUuids = guidArray
            });
        }

        public void StopAdvertising()
        {

        }

        public async void SetUpServer()
        {
            var server = CrossBleAdapter.Current.CreateGattServer();
            var service = server.AddService(Guid.NewGuid(), true);

            var characteristic = service.AddCharacteristic(
                Guid.NewGuid(),
                CharacteristicProperties.Read | CharacteristicProperties.Write | CharacteristicProperties.WriteNoResponse,
                GattPermissions.Read | GattPermissions.Write
            );

            var notifyCharacteristic = service.AddCharacteristic
            (
                Guid.NewGuid(),
                CharacteristicProperties.Indicate | CharacteristicProperties.Notify,
                GattPermissions.Read | GattPermissions.Write
            );

            IDisposable notifyBroadcast = null;
            notifyCharacteristic.WhenDeviceSubscriptionChanged().Subscribe(e =>
            {
                var @event = e.IsSubscribed ? "Subscribed" : "Unsubcribed";

                if (notifyBroadcast == null)
                {
                    this.notifyBroadcast = Observable
                        .Interval(TimeSpan.FromSeconds(1))
                        .Where(x => notifyCharacteristic.SubscribedDevices.Count > 0)
                        .Subscribe(_ =>
                        {
                            Debug.WriteLine("Sending Broadcast");
                            var dt = DateTime.Now.ToString("g");
                            var bytes = Encoding.UTF8.GetBytes(dt);
                            notifyCharacteristic.Broadcast(bytes);
                        });
                }
            });

            characteristic.WhenReadReceived().Subscribe(x =>
            {
                var write = "HELLO";

                // you must set a reply value
                x.Value = Encoding.UTF8.GetBytes(write);

                x.Status = GattStatus.Success; // you can optionally set a status, but it defaults to Success
            });
            characteristic.WhenWriteReceived().Subscribe(x =>
            {
                var write = Encoding.UTF8.GetString(x.Value, 0, x.Value.Length);
                // do something value
            });

            await server.Start(new AdvertisementData
            {
                LocalName = "TestServer"
            });
        }
    }
}

Solution

  • Ensure you have the correct key set in the info.plist. See this link for more details.

    There is a problem in that the underlying CBPeripheralManager is still in an unknown state when trying to create the Gatt Server. To work around this, you have to bypass the CrossBleAdapter and handle the creation of the top-level objects yourself. There are also some subtle changes to the API from the example given which makes this less than straight forward to get working.

    Code

    A full working example can be found here: https://github.com/jameswestgate/BleRedux

    Define an interface which you can then implement on each platform:

    public interface IBleServer
    {
        void Initialise();
    
        event EventHandler<Plugin.BluetoothLE.AdapterStatus> StatusChanged;
    
        IGattService CreateService(Guid uuid, bool primary);
        void AddService(IGattService service);
        void StartAdvertiser(AdvertisementData advertisingData);
        void StopAdvertiser();
    }
    

    Implement the interface, creating your own CBPeripheralManager and exposing the underlying StatusChanged event. On iOS, you will also need to create an Advertiser.

    public class BleServer: IBleServer
    {
        private CBPeripheralManager _manager;
        private GattServer _server;
        private Advertiser _advertiser;
    
        public event EventHandler<Plugin.BluetoothLE.AdapterStatus> StatusChanged;
    
        public void Initialise()
        {
            _manager = new CBPeripheralManager();
    
            _manager.StateUpdated += (object sender, EventArgs e) =>
            {
                var result = Plugin.BluetoothLE.AdapterStatus.Unknown;
    
                Enum.TryParse(_manager.State.ToString(), true, out result);
    
                StatusChanged?.Invoke(this, result);
            };
        }
    
        public IGattService CreateService(Guid uuid, bool primary)
        {
            if (_server == null) _server = new GattServer(_manager);
    
            return new GattService(_manager, _server, uuid, primary);
        }
    
        public void AddService(IGattService service)
        {
            _server.AddService(service);
        }
    
        public void StartAdvertiser(Plugin.BluetoothLE.Server.AdvertisementData advertisingData)
        {
            //advertisingData.ManufacturerData = new ManufacturerData();
            if (_advertiser != null) StopAdvertiser();
    
            _advertiser = new Advertiser(_manager);
    
            _advertiser.Start(advertisingData);
        }
    
        public void StopAdvertiser()
        {
            if (_advertiser == null) return;
            _advertiser.Stop();
        }
    } 
    

    From your shared project, create an instance of the class, and add your services and characteristics as per the original example:

    public partial class MainPage : ContentPage
    {
        IBleServer _server;
        IDisposable notifyBroadcast = null;
        Plugin.BluetoothLE.Server.IGattService _service;
    
        public MainPage()
        {
            InitializeComponent();
        }
    
        void Button_Clicked(System.Object sender, System.EventArgs e)
        {
            if (_server == null)
            {
                Console.WriteLine("CREATING SERVER");
                _server = DependencyService.Get<IBleServer>();
                _server.Initialise();
    
                _server.StatusChanged += Peripheral_StatusChanged;
            }
        }
    
        private void Peripheral_StatusChanged(object sender, AdapterStatus status)
        {
            try
            {
                Console.WriteLine($"GOT STATUS CHANGED: {status}");
    
                if (status != AdapterStatus.PoweredOn) return;
                if (_service != null) return;
    
                Console.WriteLine($"CREATING SERVICE");
                _service = _server.CreateService(new Guid(BluetoothConstants.kFidoServiceUUID), true);
    
                Console.WriteLine($"ADDING CHARACTERISTICS");
                var characteristic = _service.AddCharacteristic(
                    Guid.NewGuid(),
                    CharacteristicProperties.Read | CharacteristicProperties.Write | CharacteristicProperties.WriteNoResponse,
                    GattPermissions.Read | GattPermissions.Write
                );
    
                var notifyCharacteristic = _service.AddCharacteristic
                (
                    Guid.NewGuid(),
                    CharacteristicProperties.Indicate | CharacteristicProperties.Notify,
                    GattPermissions.Read | GattPermissions.Write
                );
    
                Console.WriteLine($"SUBSCRIBING TO DEVICE SUBS");
    
                notifyCharacteristic.WhenDeviceSubscriptionChanged().Subscribe(e =>
                {
                    var @event = e.IsSubscribed ? "Subscribed" : "Unsubcribed";
    
                    if (notifyBroadcast == null)
                    {
                        this.notifyBroadcast = Observable
                            .Interval(TimeSpan.FromSeconds(1))
                            .Where(x => notifyCharacteristic.SubscribedDevices.Count > 0)
                            .Subscribe(_ =>
                            {
                                Console.WriteLine("Sending Broadcast");
                                var dt = DateTime.Now.ToString("g");
                                var bytes = Encoding.UTF8.GetBytes(dt);
                                notifyCharacteristic.Broadcast(bytes);
                            });
                    }
                });
    
                Console.WriteLine($"SUBSCRIBING TO READ");
                characteristic.WhenReadReceived().Subscribe(x =>
                {
                    Console.WriteLine($"READ RECEIVED");
                    var write = "HELLO";
    
                    // you must set a reply value
                    x.Value = Encoding.UTF8.GetBytes(write);
                    x.Status = GattStatus.Success; // you can optionally set a status, but it defaults to Success
                });
    
                Console.WriteLine($"SUBSCRIBING TO WRITE");
                characteristic.WhenWriteReceived().Subscribe(x =>
                {
                    var write = Encoding.UTF8.GetString(x.Value, 0, x.Value.Length);
                    // do something value
                    Console.WriteLine($"WRITE RECEIVED: {write}");
                });
    
                //Also start advertiser (on ios)
                var advertisingData = new AdvertisementData
                {
                    LocalName = "FIDO Test Server",
                    ServiceUuids = new List<Guid> { new Guid(BluetoothConstants.kFidoServiceUUID) } //new Guid(DeviceInformationService),
                };
    
                //Now add the service
                Console.WriteLine($"ADDING SERVICE");
                _server.AddService(_service);
    
                Console.WriteLine($"STARTING ADVERTISER");
                _server.StartAdvertiser(advertisingData);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"EXCEPTION: {ex}");
            }
        }
    }
    

    Using a tool such as nFR Connect, you should now be able to connect to the server and interrogate the characteristics. You may need to encode data as utf-8 when writing (see image)

    nRF write example