Skip to content

Commit

Permalink
BlueZ bluetooth backend implementation
Browse files Browse the repository at this point in the history
sharpbrick#64 non-breaking
  • Loading branch information
rickjansen-dev committed Oct 21, 2020
1 parent 9a3831e commit f9482b7
Show file tree
Hide file tree
Showing 11 changed files with 1,387 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/SharpBrick.PoweredUp.BlueZ/BlueZConstants.cs
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 src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothAdapter.cs
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]);
}
}
}
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 src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothDevice.cs
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 src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothService.cs
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 src/SharpBrick.PoweredUp.BlueZ/DBusConnectionExtensions.cs
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, "/");
}
}
Loading

0 comments on commit f9482b7

Please sign in to comment.