2

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"
            });
        }
    }
}
Cfun
  • 8,442
  • 4
  • 30
  • 62
Oliver Spencer
  • 2,021
  • 3
  • 16
  • 22
  • An update to this. It appears the example code is quite out of date compared to the API. Digging into the samples provides ways to resolve these issues so that the code can compile - but - it still doesn't work. When trying to create a Gatt Server, the status of the adapter remains unknown. I do have a workaround that basically bypasses the adaptor, which I hope to clean up and post here if there is interest. Its also important to note the required values in the plist changed in iOS 13. – James Westgate Oct 31 '20 at 11:27

1 Answers1

0

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

James Westgate
  • 11,306
  • 8
  • 61
  • 68
  • Hi James, That's a great answer/example thank you - I've managed to get the bleutooth successfully broadcasting charateristics and I'm interrogating them with an Android device running nFR Connect as you suggest. Another issue I've just run into, I'm trying to write a byte/string from my Android to the characteristic created by the server, but nothing seems to happen - the 'waiting' indicator on the nFR Connect just spins and I don't get the 'WhenReadReceived' subscription firing. I'm guessing the read subscription isn't being called for some reason.Is there anything obviously wrong? Thank you – Oliver Spencer Nov 08 '20 at 09:58
  • Hi Oliver. I’ve noticed with nFR connect you need to choose the Utf-8 option when trying to write. – James Westgate Nov 08 '20 at 19:26
  • Hi James thanks for this. In nFR Connect there doesn't appear to be a UTF-8 option - just Byte array, byte, UNIT8/16/32, SINT8/16/32, Float16/32, TEXT & Beacon Maj/Minor - or am I looking in the wrong place? Do you know if there's some good example client code as aware that the official examples doesn't work too well. Thanks again, Oliver. – Oliver Spencer Nov 08 '20 at 20:40
  • Hi again, I've added an image of how I manage to get a write working, this is from an iPad running iOS 14. There isn;t really any other choice at the moment for a Xamarin compatible library that supports a BLE peripheral (it is not working in the Shiny library either), and not supported in the other major BLE plugin, so I am tempted to refactor the code in my sample repo into a standalone library using this plugin as a reference. – James Westgate Nov 09 '20 at 10:41
  • As an update, I've managed to get everything working fine on iOS to iOS, however again, when I try to read or write via Android, it hangs on the 'await scanReult.Device.WriteCharacteristic(xxxxxx' line. Any help would be greatly appreciated - I'm wondering whether it's something wrong with the underlying code and formatting between the two platforms. – Oliver Spencer Nov 21 '20 at 08:17
  • Try running running this on the main thread on Android maybe? See `MainThread` in Xamarin Essentials. – James Westgate Nov 21 '20 at 13:53
  • 1
    Hi James - thanks again for getting back to me. After 10 hours of playing around, I've found that changing the kFidoServiceUUID from 0000fffd-0000-1000-8000-00805f9b34fb to something else (e.g. 0000fffc-0000-1000-8000-00805f9b34fb), then works fine. Having googled the offending UUID, it looks like it could be added to a blocklist somewhere along the line: https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt. I'm not sure I'm now adopting best practice by making up my own service UUIDs, but at least it works reliably! – Oliver Spencer Nov 21 '20 at 14:21
  • Hi James/all - has anyone managed to get the GATT Bluetooth server running on Android? So far I've got everything working (iPhone server - iPhone client), (iPhone server - Android client), however I can't seem to get the Android bluetooth server working at all. James - I see in your example that the Android BleServer.cs wasn't implemented - have you had a go to implement it? So far I've created the server, and the lines of code execute to create an advertiser, but it doesn't seem to be doing anything... – Oliver Spencer Nov 28 '20 at 13:41
  • Hi Oliver. I need to give it a go. It’s a bit of a hassle given I need an Android device. It usually involves looking through Ritchies plugin code and pulling the code across into my project and trying to get it to work. Also ensure correct permissions are setup and be aware of any threading issues. – James Westgate Nov 28 '20 at 13:59
  • Thank you James - yes I'm trying from my end too. I've now managed to get the service to show up, but it's the only service shown for my Android device (no other standard services like battery level etc), and the characteristics aren't shown, so I'm guessing there's some issue in getting the AddCharacteristic function to work. Goodness me this is an uphill struggle! – Oliver Spencer Nov 28 '20 at 14:11
  • Hi @OliverSpencer, I've pushed an Android first pass code commit to the repo. I suggest we carry collaborating there via issues, pull requests etc. – James Westgate Dec 04 '20 at 18:32