Skip to content
This repository has been archived by the owner on Feb 18, 2024. It is now read-only.

Commit

Permalink
Added iOS BLE advertisement and discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-baumann committed Dec 1, 2023
1 parent a67bf00 commit 45259e3
Show file tree
Hide file tree
Showing 28 changed files with 652 additions and 48 deletions.
7 changes: 7 additions & 0 deletions SMTSP.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sources", "Sources", "{AF40
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{7010AC18-820A-4234-BBFF-82B22109B408}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMTSP.BluetoothLowEnergy", "Sources\SMTSP.BluetoothLowEnergy\SMTSP.BluetoothLowEnergy.csproj", "{666E196C-D827-4B7F-88C8-EE00108BB228}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -28,10 +30,15 @@ Global
{E9FD217B-3255-4495-BD3B-D50B90D9EBAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9FD217B-3255-4495-BD3B-D50B90D9EBAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9FD217B-3255-4495-BD3B-D50B90D9EBAB}.Release|Any CPU.Build.0 = Release|Any CPU
{666E196C-D827-4B7F-88C8-EE00108BB228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{666E196C-D827-4B7F-88C8-EE00108BB228}.Debug|Any CPU.Build.0 = Debug|Any CPU
{666E196C-D827-4B7F-88C8-EE00108BB228}.Release|Any CPU.ActiveCfg = Release|Any CPU
{666E196C-D827-4B7F-88C8-EE00108BB228}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E9FD217B-3255-4495-BD3B-D50B90D9EBAB} = {AF403CDB-D1CB-44DD-81C7-B5B822941D43}
{FC4F2C45-E24B-45DD-831E-648EB107847A} = {AF403CDB-D1CB-44DD-81C7-B5B822941D43}
{13A8E9DB-1627-4306-9E75-21B7118C5158} = {7010AC18-820A-4234-BBFF-82B22109B408}
{666E196C-D827-4B7F-88C8-EE00108BB228} = {AF403CDB-D1CB-44DD-81C7-B5B822941D43}
EndGlobalSection
EndGlobal
10 changes: 10 additions & 0 deletions Sources/SMTSP.BluetoothLowEnergy/BleClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace SMTSP.BluetoothLowEnergy;

public partial class BleClient(byte[] deviceData)
{
public bool IsResponding { get; private set; }

public partial Task<bool> RequestAccess();
public partial void StartRespondingToDiscoveryBroadcasts();
public partial void StopRespondingToDiscoveryBroadcasts();
}
15 changes: 15 additions & 0 deletions Sources/SMTSP.BluetoothLowEnergy/BleServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace SMTSP.BluetoothLowEnergy;

public partial class BleServer
{
public event EventHandler<byte[]> PeripheralDataDiscovered = delegate { };
public event EventHandler<Stream> ClientConnected = delegate { };

public partial Task<bool> RequestAccess();

public partial Task<ushort> StartServer();
public partial void StopServer();

public partial void StartDiscovering();
public partial void StopDiscovering();
}
7 changes: 7 additions & 0 deletions Sources/SMTSP.BluetoothLowEnergy/Core.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SMTSP.BluetoothLowEnergy;

public static class Core
{
public const string ServiceUuid = "68D60EB2-8AAA-4D72-8851-BD6D64E169B7";
public const string CharacteristicUuid = "0BEBF3FE-9A5E-4ED1-8157-76281B3F0DA5";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace SMTSP.BluetoothLowEnergy;

public partial class BleClient
{
public partial Task<bool> RequestAccess() => throw new PlatformNotSupportedException();
public partial void StartRespondingToDiscoveryBroadcasts() => throw new PlatformNotSupportedException();
public partial void StopRespondingToDiscoveryBroadcasts() => throw new PlatformNotSupportedException();
}
12 changes: 12 additions & 0 deletions Sources/SMTSP.BluetoothLowEnergy/Platforms/Android/BleServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace SMTSP.BluetoothLowEnergy;

public partial class BleServer
{
public partial Task<bool> RequestAccess() => throw new PlatformNotSupportedException();

public partial Task<ushort> StartServer() => throw new PlatformNotSupportedException();
public partial void StopServer() => throw new PlatformNotSupportedException();

public partial void StartDiscovering() => throw new PlatformNotSupportedException();
public partial void StopDiscovering() => throw new PlatformNotSupportedException();
}
116 changes: 116 additions & 0 deletions Sources/SMTSP.BluetoothLowEnergy/Platforms/Apple/BleClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using CoreBluetooth;

namespace SMTSP.BluetoothLowEnergy;

public partial class BleClient : CBCentralManagerDelegate
{
private CBCentralManager? _manager;
private readonly TaskCompletionSource<bool> _stateTaskCompletionSource = new();
private readonly NSData _deviceData = NSData.FromArray(deviceData);
private readonly CBUUID _nativeServiceUuid = CBUUID.FromString(Core.ServiceUuid);
private readonly CBUUID _nativeCharacteristicUuid = CBUUID.FromString(Core.CharacteristicUuid);

public CBCentralManager Manager
{
get
{
if (_manager == null)
{
_manager = new CBCentralManager(this, null);
_manager.Delegate = this;
}

return _manager;
}
}

public partial Task<bool> RequestAccess()
{
Extensions.EnsureAllowed();

if (Manager.State != CBManagerState.Unknown)
{
return Task.FromResult(true);
}

_ = Manager.State;

return _stateTaskCompletionSource.Task;
}

public partial void StartRespondingToDiscoveryBroadcasts()
{
if (Manager.IsScanning)
{
return;
}

Manager.ScanForPeripherals([_nativeServiceUuid], new PeripheralScanningOptions
{
AllowDuplicatesKey = true
});

IsResponding = true;
}

public partial void StopRespondingToDiscoveryBroadcasts()
{
IsResponding = false;
Manager.StopScan();
}

public override void UpdatedState(CBCentralManager central)
{
_stateTaskCompletionSource.TrySetResult(Manager.State == CBManagerState.PoweredOn);
}

public override void DiscoveredPeripheral(CBCentralManager central, CBPeripheral peripheral, NSDictionary advertisementData, NSNumber rssi)
{
if (!IsResponding)
{
return;
}

Manager.ConnectPeripheral(peripheral);
}

private void PeripheralOnDiscoveredService(object? sender, NSErrorEventArgs e)
{
if (sender is not CBPeripheral cbPeripheral) { return; }

var service = cbPeripheral.Services?.FirstOrDefault(
element => element.UUID == _nativeServiceUuid
);

if (service == null)
{
return;
}

cbPeripheral.DiscoverCharacteristics([_nativeCharacteristicUuid], service);
}

private void PeripheralOnDiscoveredCharacteristics(object? sender, CBServiceEventArgs args)
{
if (IsResponding)
{
var characteristic = args.Service.Characteristics?.FirstOrDefault(element =>
element.UUID == _nativeCharacteristicUuid);

if (characteristic == null)
{
return;
}

args.Service.Peripheral?.WriteValue(_deviceData, characteristic, CBCharacteristicWriteType.WithoutResponse);
}
}

public override void ConnectedPeripheral(CBCentralManager central, CBPeripheral nativePeripheral)
{
nativePeripheral.DiscoveredService += PeripheralOnDiscoveredService;
nativePeripheral.DiscoveredCharacteristics += PeripheralOnDiscoveredCharacteristics;

nativePeripheral.DiscoverServices([_nativeServiceUuid]);
}
}
155 changes: 155 additions & 0 deletions Sources/SMTSP.BluetoothLowEnergy/Platforms/Apple/BleServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using CoreBluetooth;

namespace SMTSP.BluetoothLowEnergy;

public partial class BleServer
{
private CBPeripheralManager? _manager;
protected CBPeripheralManager Manager
{
get
{
// var options = new NSDictionary(
// CBPeripheralManager.OptionRestoreIdentifierKey, "com.julian-baumann.smtsp"
// );

// _manager ??= new CBPeripheralManager(peripheralDelegate: null, queue: null, options: options);
_manager ??= new CBPeripheralManager();
return _manager;
}
}

private ushort _psm;

public partial Task<bool> RequestAccess()
{
Extensions.EnsureAllowed();

if (Manager.State != CBManagerState.Unknown)
{
return Task.FromResult(true);
}

var result = new TaskCompletionSource<bool>();

Manager.StateUpdated += (_, _) =>
{
result.SetResult(Manager.State == CBManagerState.PoweredOn);
};

_ = Manager.State;

return result.Task;
}

private async Task<ushort> PublishL2Cap(bool secure)
{
var taskCompletionSource = new TaskCompletionSource<ushort>();

var handler = new EventHandler<CBPeripheralManagerL2CapChannelOperationEventArgs>((_, args) =>
{
if (args.Error == null)
{
taskCompletionSource.TrySetResult(args.Psm);
}
else
{
taskCompletionSource.TrySetException(new InvalidOperationException(args.Error.Description));
}
});

Manager.DidPublishL2CapChannel += handler;

try
{
Manager.PublishL2CapChannel(secure);
return await taskCompletionSource.Task.ConfigureAwait(false);
}
finally
{
Manager.DidPublishL2CapChannel -= handler;
}
}

private void ManagerOnDidOpenL2CapChannel(object? sender, CBPeripheralManagerOpenL2CapChannelEventArgs args)
{
//args.Channel.InputStream.Status == NSStreamStatus.Open
var channel = args.Channel!;
channel.InputStream.Open();
channel.OutputStream.Open();

ClientConnected.Invoke(this, new L2CapStream(channel.OutputStream, channel.InputStream));
}

private void AdvertiseService()
{
var service = new CBMutableService(CBUUID.FromString(Core.ServiceUuid), true);

var characteristic = new CBMutableCharacteristic(
uuid: CBUUID.FromString(Core.CharacteristicUuid),
properties: CBCharacteristicProperties.Read | CBCharacteristicProperties.Write,
value: null,
permissions: CBAttributePermissions.Readable | CBAttributePermissions.Writeable
);

service.Characteristics = [characteristic];

Manager.WriteRequestsReceived += ManagerOnWriteRequestsReceived;

Manager.AddService(service);
Manager.StartAdvertising(new StartAdvertisingOptions
{
ServicesUUID = [CBUUID.FromString(Core.ServiceUuid)]
});
}

private void ManagerOnWriteRequestsReceived(object? sender, CBATTRequestsEventArgs args)
{
foreach (var request in args.Requests)
{
var value = request.Value;

if (value == null)
{
return;
}

var valueAsByteArray = new byte[value.Length];
System.Runtime.InteropServices.Marshal.Copy(value.Bytes, valueAsByteArray, 0, Convert.ToInt32(value.Length));

PeripheralDataDiscovered.Invoke(this, valueAsByteArray);
}
}

public partial async Task<ushort> StartServer()
{
Manager.DidOpenL2CapChannel += ManagerOnDidOpenL2CapChannel;
_psm = await PublishL2Cap(false);

return _psm;
}

public partial void StartDiscovering()
{
if (_psm <= 0)
{
throw new InvalidOperationException("PSM unknown, did you forget to call StartServer()?");
}

AdvertiseService();
}

public partial void StopDiscovering()
{
Manager.StopAdvertising();
Manager.RemoveAllServices();
}

public partial void StopServer()
{
if (_psm <= 0) { return; }

Manager.UnpublishL2CapChannel(_psm);
Manager.DidOpenL2CapChannel -= ManagerOnDidOpenL2CapChannel;
}
}
26 changes: 26 additions & 0 deletions Sources/SMTSP.BluetoothLowEnergy/Platforms/Apple/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace SMTSP.BluetoothLowEnergy;

public static class Extensions
{
public static bool HasPlistValue(string key)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
return NSBundle.MainBundle.ObjectForInfoDictionary(key) != null;
}

public static void EnsureAllowed()
{
var hasPeripheralUsage = HasPlistValue("NSBluetoothPeripheralUsageDescription");
var hasAlwaysUsage = HasPlistValue("NSBluetoothAlwaysUsageDescription");

if (!hasPeripheralUsage)
{
throw new UnauthorizedAccessException("\"NSBluetoothPeripheralUsageDescription\" is not set.");
}

if (!hasAlwaysUsage)
{
throw new UnauthorizedAccessException("\"NSBluetoothAlwaysUsageDescription\" is not set.");
}
}
}
Loading

0 comments on commit 45259e3

Please sign in to comment.