forked from sharpbrick/powered-up
-
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.
BlueZ bluetooth backend implementation
sharpbrick#64 non-breaking
- Loading branch information
1 parent
9a3831e
commit f9482b7
Showing
11 changed files
with
1,387 additions
and
0 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
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"; | ||
} | ||
} | ||
|
134 changes: 134 additions & 0 deletions
134
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,134 @@ | ||
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 = Connection.System.CreateProxy<IAdapter1>(BlueZConstants.BlueZDBusServiceName, _adapterObjectPath) ?? await FindFirstAdapter(); | ||
|
||
// validate the adapter | ||
await adapter.GetAliasAsync(); | ||
|
||
await AttachEventHandlers(); | ||
|
||
return adapter; | ||
} | ||
|
||
private async Task<IAdapter1> FindFirstAdapter() | ||
{ | ||
var adapters = await Connection.System.FindProxies<IAdapter1>(); | ||
return adapters.FirstOrDefault(); | ||
} | ||
|
||
private async Task AttachEventHandlers() | ||
=> await _adapter.WatchPropertiesAsync(DevicePropertyChangedHandler); | ||
|
||
private void DevicePropertyChangedHandler(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 () => | ||
{ | ||
await _adapter.StopDiscoveryAsync(); | ||
}); | ||
|
||
await _adapter.StartDiscoveryAsync(); | ||
|
||
async void NewDeviceAddedHandler((ObjectPath objectPath, IDictionary<string, IDictionary<string, object>> interfaces) args) | ||
{ | ||
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]); | ||
} | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
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,63 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
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 _characteristic.WriteValueAsync(data, new Dictionary<string,object>()); | ||
|
||
// await Policy | ||
// .Handle<Tmds.DBus.DBusException>() | ||
// .WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(100)) | ||
// .ExecuteAsync(() => _characteristic.WriteValueAsync(data, new Dictionary<string, object>())); | ||
|
||
return true; | ||
} | ||
} | ||
} |
110 changes: 110 additions & 0 deletions
110
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,110 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
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(); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
public async Task<IPoweredUpBluetoothService> GetServiceAsync(Guid serviceId) | ||
{ | ||
var connectionTimeout = TimeSpan.FromSeconds(5); | ||
|
||
await _device.ConnectAsync(); | ||
|
||
await Task.Delay(connectionTimeout); | ||
|
||
if (!Connected || !ServicesResolved) | ||
{ | ||
throw new TimeoutException($"Connection timeout out after {connectionTimeout.Seconds}"); | ||
} | ||
|
||
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"); | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothService.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,43 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
using SharpBrick.PoweredUp.Bluetooth; | ||
using Tmds.DBus; | ||
|
||
namespace SharpBrick.PoweredUp.BlueZ | ||
{ | ||
internal class BlueZPoweredUpBluetoothService : IPoweredUpBluetoothService | ||
{ | ||
private IGattService1 _gattService; | ||
|
||
public BlueZPoweredUpBluetoothService(IGattService1 gattService, Guid uuid) | ||
{ | ||
Uuid = uuid; | ||
_gattService = gattService; | ||
} | ||
|
||
public Guid Uuid { get; } | ||
|
||
public void Dispose() | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
public async Task<IPoweredUpBluetoothCharacteristic> GetCharacteristicAsync(Guid characteristicId) | ||
{ | ||
var characteristics = await Connection.System.FindProxies<IGattCharacteristic1>(); | ||
|
||
foreach (var characteristic in characteristics) | ||
{ | ||
var characteristicUuid = Guid.Parse(await characteristic.GetUUIDAsync()); | ||
|
||
if (characteristicUuid == characteristicId) | ||
{ | ||
return new BlueZPoweredUpBluetoothCharacteristic(characteristic, characteristicId); | ||
} | ||
} | ||
|
||
throw new ArgumentOutOfRangeException(nameof(characteristicId), $"Characteristic with id {characteristicId} not found"); | ||
|
||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
src/SharpBrick.PoweredUp.BlueZ/DBusConnectionExtensions.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,30 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Tmds.DBus; | ||
|
||
namespace SharpBrick.PoweredUp.BlueZ | ||
{ | ||
internal static class DBusConnectionExtensions | ||
{ | ||
internal static async Task<ICollection<T>> FindProxies<T>(this Connection connection) where T : IDBusObject | ||
{ | ||
var dbusInterfaceAttribute = typeof(T).GetCustomAttributes(false).Cast<DBusInterfaceAttribute>().First(); | ||
var objects = await GetObjectManager(connection).GetManagedObjectsAsync(); | ||
|
||
return objects | ||
.Where(x => x.Value.ContainsKey(dbusInterfaceAttribute.Name)) | ||
.Select(x => Connection.System.CreateProxy<T>(BlueZConstants.BlueZDBusServiceName, x.Key)) | ||
.ToList(); | ||
} | ||
|
||
internal static async Task WatchInterfacesAdded(this Connection connection, Action<(ObjectPath objectPath, IDictionary<string, IDictionary<string, object>> interfaces)> handler) | ||
{ | ||
var disposable = await GetObjectManager(connection).WatchInterfacesAddedAsync(handler); | ||
} | ||
|
||
private static IObjectManager GetObjectManager(Connection connection) | ||
=> connection.CreateProxy<IObjectManager>(BlueZConstants.BlueZDBusServiceName, "/"); | ||
} | ||
} |
Oops, something went wrong.