Reputation: 623
I am working on a Xamarin.Forms (version 4.8.0.1687) cross-platform app that needs to be able to talk to other instances of itself using Bluetooth LE. I am taking the shared interfaces/platform-specific implementations approach to Bluetooth because we plan to eventually expand the supported platforms to include some that are not all supported by any one of the well-known packages. The version I am on right now is really just a proof-of-concept, and I am not yet doing extensive error checking or handling of edge cases, so the code is still fairly simple.
I have been able to advertise on both Android and iOS platforms, and to see, via service UUID-filtered scan, other instances of the app that are advertising, from both platforms. But I cannot establish a connection from either. On Android, calling BluetoothSocket.Connect()
consistently results in it throwing a Java.IO.IOException with the message read failed, socket might closed or timeout, read ret: -1 after a delay of several seconds. On iOS, calling CBCentralManager.ConnectPeripheral(CBPeripheral)
never raises a ConnectedPeripheral event, never raises a FailedToConnectPeripheral event, and never throws an exception. If I call it in a loop waiting for the peripheral's State to change, the loop simply runs indefinitely.
The fact that I am hitting a snag at the same place on both platforms suggests to me that I'm misunderstanding something about the Bluetooth process rather than the code itself, which would not be terribly surprising since this is the first time I've done any Bluetooth API programming on any platform. But I'm just guessing. If anyone can give me a hint about where I've gone wrong with either API you'll have my heartfelt gratitude which, together with $4, will get you a cup of decent coffee.
Android code
The Android class that does the advertising (works):
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);
}
}
}
The Android class that does the scanning (works):
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;
}
}
The Android class that does the connecting (never works):
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 Code
The iOS class that does the advertising (works):
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());
}
}
}
The iOS class that does the scanning (works):
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, BluetoothConstants.DEFAULT_SCAN_TIMEOUT);
}
public async Task<List<IPeripheral>> ScanForService(string 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());
}
}
}
The iOS class that does the connecting (never works):
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);
}
}
EDIT: I have been able to get the Android app to connect, at least. I was apparently using incorrect classes; it seems that most of the connection examples I found were referring to Bluetooth classic. For me, at least, this works (obviously it will need error checking before it moves forward):
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;
}
}
I am still unable to get the iOS app to connect to the Android app or to another instance of the iOS app. Every example I have found that claims to be a working example uses exactly the same calls that I do, so investigation and head-scratching continue.
Upvotes: 0
Views: 214
Reputation: 623
As noted in the edit above, the solution in the Android case was to call ConnectGatt() on the BluetoothDevice object.
After days working though Apple doc and tutorials what I realized I was doing wrong was keeping the CBPeripheral instance representing the found peripheral, but not necessarily the CBCentralManager instance that discovered it. In iOS you must call ConnectPeripheral() on the same instance of CBCentralManager that scanned and discovered the peripheral. I refactored the shared project in a way that permitted preserving the instance, and now I can connect from either OS to either OS.
Upvotes: 1