Skip to content

Commit

Permalink
BlueZ bluetooth backend implementation
Browse files Browse the repository at this point in the history
#64 non-breaking
  • Loading branch information
rickjansen-dev committed Oct 23, 2020
1 parent 9a3831e commit 8edf0dc
Show file tree
Hide file tree
Showing 15 changed files with 1,418 additions and 3 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var serviceProvider = new ServiceCollection()
.AddLogging()
.AddPoweredUp()
.AddWinRTBluetooth() // using WinRT Bluetooth on Windows
//.AddBlueZBluetooth() // using BlueZ Bluetooth on Linux
.BuildServiceProvider();

var host = serviceProvider.GetService<PoweredUpHost>();
Expand Down Expand Up @@ -155,6 +156,7 @@ var serviceProvider = new ServiceCollection()
.AddLogging()
.AddPoweredUp()
.AddWinRTBluetooth() // using WinRT Bluetooth on Windows
//.AddBlueZBluetooth() // using BlueZ Bluetooth on Linux
.BuildServiceProvider();

using (var scope = serviceProvider.CreateScope()) // create a scoped DI container per intented active connection/protocol. If disposed, disposes all disposable artifacts.
Expand Down Expand Up @@ -236,6 +238,8 @@ DI Container Elements
- [X] .NET Core 3.1 (on Windows 10 using WinRT)
- Library uses `Span<T>` / C# 8.0 and is therefore not supported in .NET Framework 1.0 - 4.8 and UWP Apps until arrival of .NET 5 (WinForms and WPF work in .NET Core 3.1)
- Library uses WinRT for communication therefore only Windows 10
- [X] .NET Core 3.1 / .NET 5 on Linux using BlueZ
- Requires `bluez` to be installed and configured.
- [ ] Xamarin (on iOS / Android using ?)
- [ ] Blazor (on Browser using WebBluetooth)
- Hub Model
Expand Down
2 changes: 1 addition & 1 deletion examples/SharpBrick.PoweredUp.Examples/BaseExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void InitHost(bool enableTrace)
builder.AddFilter("SharpBrick.PoweredUp.Bluetooth.BluetoothKernel", LogLevel.Debug);
}
})
.AddWinRTBluetooth()
.AddBlueZBluetooth()
;

Configure(serviceCollection);
Expand Down
4 changes: 2 additions & 2 deletions examples/SharpBrick.PoweredUp.Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ static async Task Main(string[] args)
//example = new Example.ExampleMotorVirtualPort();
//example = new Example.ExampleHubActions();
//example = new Example.ExampleTechnicMediumHubAccelerometer();
//example = new Example.ExampleTechnicMediumHubGyroSensor();
example = new Example.ExampleTechnicMediumHubGyroSensor();
//example = new Example.ExampleVoltage();
//example = new Example.ExampleTechnicMediumTemperatureSensor();
//example = new Example.ExampleMotorInputCombinedMode();
Expand All @@ -33,7 +33,7 @@ static async Task Main(string[] args)
//example = new Example.ExampleHubPropertyObserving();
//example = new Example.ExampleDiscoverByType();
//example = new Example.ExampleCalibrationSteering();
example = new Example.ExampleTechnicMediumHubGestSensor();
//example = new Example.ExampleTechnicMediumHubGestSensor();

// NOTE: Examples are programmed object oriented style. Base class implements methods Configure, DiscoverAsync and ExecuteAsync to be overwriten on demand.
await example.InitHostAndDiscoverAsync(enableTrace);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SharpBrick.PoweredUp\SharpBrick.PoweredUp.csproj" />
<ProjectReference Include="..\..\src\SharpBrick.PoweredUp.WinRT\SharpBrick.PoweredUp.WinRT.csproj" />
<ProjectReference Include="..\..\src\SharpBrick.PoweredUp.BlueZ\SharpBrick.PoweredUp.BlueZ.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
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";
}
}

139 changes: 139 additions & 0 deletions src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothAdapter.cs
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]);
}
}
}
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 src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothDevice.cs
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");
}
}
}
Loading

0 comments on commit 8edf0dc

Please sign in to comment.