-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BlueZ bluetooth backend implementation
#64 non-breaking
- Loading branch information
1 parent
9a3831e
commit 8edf0dc
Showing
15 changed files
with
1,418 additions
and
3 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
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
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 SharpBrick.PoweredUp.BlueZ | ||
{ | ||
internal class BlueZConstants | ||
{ | ||
public const string BlueZDBusServiceName = "org.bluez"; | ||
} | ||
} | ||
|
139 changes: 139 additions & 0 deletions
139
src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothAdapter.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,139 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Logging; | ||
using SharpBrick.PoweredUp.Bluetooth; | ||
using SharpBrick.PoweredUp.BlueZ.Utilities; | ||
using Tmds.DBus; | ||
|
||
namespace SharpBrick.PoweredUp.BlueZ | ||
{ | ||
public class BlueZPoweredUpBluetoothAdapter : IPoweredUpBluetoothAdapter | ||
{ | ||
private readonly ILogger<BlueZPoweredUpBluetoothAdapter> _logger; | ||
private readonly string _adapterObjectPath; | ||
private readonly Dictionary<ulong, IPoweredUpBluetoothDevice> _devices = new Dictionary<ulong, IPoweredUpBluetoothDevice>(); | ||
private IAdapter1 _adapter; | ||
|
||
public bool Discovering { get; set; } = false; | ||
|
||
public BlueZPoweredUpBluetoothAdapter( | ||
ILogger<BlueZPoweredUpBluetoothAdapter> logger, | ||
string adapterObjectPath = null) //"/org/bluez/hci0") | ||
{ | ||
_logger = logger; | ||
_adapterObjectPath = adapterObjectPath; | ||
} | ||
|
||
private async Task<IAdapter1> GetAdapterAsync() | ||
{ | ||
var adapter = !string.IsNullOrEmpty(_adapterObjectPath) ? Connection.System.CreateProxy<IAdapter1>(BlueZConstants.BlueZDBusServiceName, _adapterObjectPath) : await FindFirstAdapter(); | ||
|
||
// validate the adapter | ||
await adapter.GetAliasAsync(); | ||
|
||
await adapter.WatchPropertiesAsync(AdapterPropertyChangedHandler); | ||
|
||
return adapter; | ||
} | ||
|
||
private async Task<IAdapter1> FindFirstAdapter() | ||
{ | ||
var adapters = await Connection.System.FindProxies<IAdapter1>(); | ||
return adapters.FirstOrDefault(); | ||
} | ||
|
||
private void AdapterPropertyChangedHandler(PropertyChanges changes) | ||
{ | ||
_logger.LogDebug("Property changed {ChangedProperties}", changes.Changed); | ||
|
||
foreach (var propertyChanged in changes.Changed) | ||
{ | ||
switch (propertyChanged.Key) | ||
{ | ||
case "Discovering": | ||
Discovering = (bool)propertyChanged.Value; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private async Task<ICollection<IDevice1>> GetExistingDevicesAsync() | ||
=> await Connection.System.FindProxies<IDevice1>(); | ||
|
||
private IDevice1 GetSpecificDeviceAsync(ObjectPath objectPath) | ||
=> Connection.System.CreateProxy<IDevice1>(BlueZConstants.BlueZDBusServiceName, objectPath); | ||
|
||
//private IPoweredUpBluetoothDevice ConstructDevice(IDevice1 device, Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler = null) | ||
// => new BlueZPoweredUpBluetoothDevice(device, discoveryHandler); | ||
|
||
private async Task<bool> IsLegoWirelessProcotolDevice(IDevice1 device) | ||
=> (await device.GetUUIDsAsync()).NullToEmpty().Any(x => x.ToUpperInvariant() == PoweredUpBluetoothConstants.LegoHubService); | ||
|
||
public async void Discover(Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler, CancellationToken cancellationToken = default) | ||
{ | ||
_adapter ??= await GetAdapterAsync(); | ||
|
||
var existingDevices = await GetExistingDevicesAsync(); | ||
|
||
foreach (var device in existingDevices) | ||
{ | ||
if (await IsLegoWirelessProcotolDevice(device)) | ||
{ | ||
var poweredUpDevice = new BlueZPoweredUpBluetoothDevice(device, discoveryHandler); | ||
await poweredUpDevice.Initialize(); | ||
|
||
_devices.Add(poweredUpDevice.DeviceInfo.BluetoothAddress, poweredUpDevice); | ||
|
||
await poweredUpDevice.GetManufacturerDataAndInvokeHandlerAsync(); | ||
} | ||
} | ||
|
||
await Connection.System.WatchInterfacesAdded(NewDeviceAddedHandler); | ||
|
||
await _adapter.SetDiscoveryFilterAsync(new Dictionary<string,object>() | ||
{ | ||
{ "UUIDs", new string[] { PoweredUpBluetoothConstants.LegoHubService } } | ||
}); | ||
|
||
cancellationToken.Register(async () => | ||
{ | ||
if (Discovering) | ||
{ | ||
await _adapter.StopDiscoveryAsync(); | ||
} | ||
}); | ||
|
||
await _adapter.StartDiscoveryAsync(); | ||
|
||
async void NewDeviceAddedHandler((ObjectPath objectPath, IDictionary<string, IDictionary<string, object>> interfaces) args) | ||
{ | ||
if (!args.interfaces.ContainsKey("org.bluez.Device1")) | ||
{ | ||
return; | ||
} | ||
|
||
var device = GetSpecificDeviceAsync(args.objectPath); | ||
var poweredUpDevice = new BlueZPoweredUpBluetoothDevice(device, discoveryHandler); | ||
|
||
await poweredUpDevice.Initialize(); | ||
|
||
_devices.Add(poweredUpDevice.DeviceInfo.BluetoothAddress, poweredUpDevice); | ||
|
||
await poweredUpDevice.GetManufacturerDataAndInvokeHandlerAsync(); | ||
} | ||
} | ||
|
||
public Task<IPoweredUpBluetoothDevice> GetDeviceAsync(ulong bluetoothAddress) | ||
{ | ||
if (!_devices.ContainsKey(bluetoothAddress)) | ||
{ | ||
throw new ArgumentOutOfRangeException("Requested bluetooth device is not available from this adapter"); | ||
} | ||
|
||
return Task.FromResult<IPoweredUpBluetoothDevice>(_devices[bluetoothAddress]); | ||
} | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothCharacteristic.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,62 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using Polly; | ||
using SharpBrick.PoweredUp.Bluetooth; | ||
using Tmds.DBus; | ||
|
||
namespace SharpBrick.PoweredUp.BlueZ | ||
{ | ||
internal class BlueZPoweredUpBluetoothCharacteristic : IPoweredUpBluetoothCharacteristic | ||
{ | ||
private IGattCharacteristic1 _characteristic; | ||
|
||
public BlueZPoweredUpBluetoothCharacteristic(IGattCharacteristic1 characteristic, Guid uuid) | ||
{ | ||
Uuid = uuid; | ||
_characteristic = characteristic ?? throw new ArgumentNullException(nameof(characteristic)); | ||
} | ||
|
||
public Guid Uuid { get; } | ||
|
||
public async Task<bool> NotifyValueChangeAsync(Func<byte[], Task> notificationHandler) | ||
{ | ||
if (notificationHandler is null) | ||
{ | ||
throw new ArgumentNullException(nameof(notificationHandler)); | ||
} | ||
|
||
await _characteristic.WatchPropertiesAsync(PropertyChangedHandler); | ||
|
||
await _characteristic.StartNotifyAsync(); | ||
|
||
return true; | ||
|
||
void PropertyChangedHandler(PropertyChanges propertyChanges) | ||
{ | ||
foreach (var propertyChanged in propertyChanges.Changed) | ||
{ | ||
if (propertyChanged.Key == "Value") | ||
{ | ||
notificationHandler((byte[])propertyChanged.Value); | ||
} | ||
} | ||
} | ||
} | ||
|
||
public async Task<bool> WriteValueAsync(byte[] data) | ||
{ | ||
if (data is null) | ||
{ | ||
throw new ArgumentNullException(nameof(data)); | ||
} | ||
|
||
await Policy | ||
.Handle<Tmds.DBus.DBusException>() | ||
.WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(10)) | ||
.ExecuteAsync(() => _characteristic.WriteValueAsync(data, new Dictionary<string, object>())); | ||
|
||
return true; | ||
} | ||
} | ||
} |
124 changes: 124 additions & 0 deletions
124
src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothDevice.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,124 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using SharpBrick.PoweredUp.Bluetooth; | ||
using Tmds.DBus; | ||
|
||
namespace SharpBrick.PoweredUp.BlueZ | ||
{ | ||
internal class BlueZPoweredUpBluetoothDevice : IPoweredUpBluetoothDevice | ||
{ | ||
private readonly Func<PoweredUpBluetoothDeviceInfo, Task> _discoveryHandler; | ||
private IDevice1 _device; | ||
|
||
internal PoweredUpBluetoothDeviceInfo DeviceInfo { get; private set; } = new PoweredUpBluetoothDeviceInfo(); | ||
internal bool Connected { get; private set; } = false; | ||
internal bool ServicesResolved { get; private set;} = false; | ||
|
||
internal BlueZPoweredUpBluetoothDevice(IDevice1 device, Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler = null) | ||
{ | ||
_discoveryHandler = discoveryHandler; | ||
_device = device; | ||
} | ||
|
||
public string Name { get; private set; } = string.Empty; | ||
|
||
internal async Task Initialize() | ||
{ | ||
await _device.WatchPropertiesAsync(DevicePropertyChangedHandler); | ||
|
||
await GetSafeDeviceInfoAsync(); | ||
} | ||
|
||
internal async Task GetManufacturerDataAndInvokeHandlerAsync() | ||
{ | ||
try | ||
{ | ||
var manufacturerData = await _device.GetManufacturerDataAsync(); | ||
DeviceInfo.ManufacturerData = (byte[])manufacturerData.First().Value; | ||
|
||
await _discoveryHandler(DeviceInfo); | ||
} | ||
catch | ||
{ | ||
// we can ignore errors here, this will throw an exception for existing devices (only after reboot) | ||
// manufacturer data will be returned when discovery is turned on | ||
} | ||
} | ||
|
||
private async void DevicePropertyChangedHandler(PropertyChanges changes) | ||
{ | ||
foreach (var propertyChanged in changes.Changed) | ||
{ | ||
switch (propertyChanged.Key) | ||
{ | ||
case "ManufacturerData": | ||
DeviceInfo.ManufacturerData = (byte[])((IDictionary<ushort,object>)propertyChanged.Value).First().Value; | ||
await _discoveryHandler(DeviceInfo); | ||
break; | ||
case "Connected": | ||
Connected = (bool)propertyChanged.Value; | ||
break; | ||
case "ServicesResolved": | ||
ServicesResolved = (bool)propertyChanged.Value; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
internal async Task GetSafeDeviceInfoAsync() | ||
{ | ||
var btAddress = await _device.GetAddressAsync(); | ||
DeviceInfo.BluetoothAddress = Utilities.BluetoothAddressFormatter.ConvertToInteger(btAddress); | ||
DeviceInfo.Name = Name = await _device.GetNameAsync(); | ||
} | ||
|
||
~BlueZPoweredUpBluetoothDevice() => Dispose(false); | ||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } | ||
|
||
protected virtual async void Dispose(bool disposing) | ||
{ | ||
if (Connected) | ||
await _device?.DisconnectAsync(); // dangerous to await here, try to find a better way | ||
|
||
_device = null; | ||
} | ||
|
||
private async Task WaitForConnectionAndServicesResolved(CancellationToken token) | ||
{ | ||
while (!Connected || !ServicesResolved) | ||
{ | ||
await Task.Delay(25, token); | ||
} | ||
} | ||
|
||
public async Task<IPoweredUpBluetoothService> GetServiceAsync(Guid serviceId) | ||
{ | ||
var connectionTimeout = TimeSpan.FromSeconds(5); | ||
|
||
var cancellationTokenSource = new CancellationTokenSource(); | ||
|
||
await _device.ConnectAsync(); | ||
|
||
cancellationTokenSource.CancelAfter(connectionTimeout); | ||
|
||
await WaitForConnectionAndServicesResolved(cancellationTokenSource.Token); | ||
|
||
var gattServices = await Connection.System.FindProxies<IGattService1>(); | ||
|
||
foreach (var gattService in gattServices) | ||
{ | ||
var gattUuid = Guid.Parse(await gattService.GetUUIDAsync()); | ||
|
||
if (gattUuid == serviceId) | ||
{ | ||
return new BlueZPoweredUpBluetoothService(gattService, gattUuid); | ||
} | ||
} | ||
|
||
throw new ArgumentOutOfRangeException(nameof(serviceId), $"Service with id {serviceId} not found"); | ||
} | ||
} | ||
} |
Oops, something went wrong.