This repository has been archived by the owner on Feb 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added iOS BLE advertisement and discovery
- Loading branch information
1 parent
a67bf00
commit 45259e3
Showing
28 changed files
with
652 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} |
8 changes: 8 additions & 0 deletions
8
Sources/SMTSP.BluetoothLowEnergy/Platforms/Android/BleClient.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
12
Sources/SMTSP.BluetoothLowEnergy/Platforms/Android/BleServer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
116
Sources/SMTSP.BluetoothLowEnergy/Platforms/Apple/BleClient.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
155
Sources/SMTSP.BluetoothLowEnergy/Platforms/Apple/BleServer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
26
Sources/SMTSP.BluetoothLowEnergy/Platforms/Apple/Extensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | ||
} | ||
} | ||
} |
Oops, something went wrong.