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 5fe9fea
Show file tree
Hide file tree
Showing 15 changed files with 1,434 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
3 changes: 2 additions & 1 deletion examples/SharpBrick.PoweredUp.Examples/BaseExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ public void InitHost(bool enableTrace)
if (enableTrace)
{
builder.AddFilter("SharpBrick.PoweredUp.Bluetooth.BluetoothKernel", LogLevel.Debug);
builder.AddFilter("SharpBrick.PoweredUp.BlueZ.BlueZPoweredUpBluetoothAdapter", 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";
}
}

142 changes: 142 additions & 0 deletions src/SharpBrick.PoweredUp.BlueZ/BlueZPoweredUpBluetoothAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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();

// make sure it is powered on
if (!await adapter.GetPoweredAsync())
{
await adapter.SetPoweredAsync(true);
}

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 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.TryGetManufacturerDataAsync();
}
}

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.TryGetManufacturerDataAsync();
}
}

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;
}
}
}
Loading

0 comments on commit 5fe9fea

Please sign in to comment.