Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BlueZ Bluetooth Stack #118

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work in adding advertisement. We can make that even more prominent in the first section "Features" or however it is called.

Also - with your agreement - I will not be shy promoting your contribution about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

- [ ] 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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think once we have this in the master, I will thinking about incorporating the Bluetooth selection into the core .AddPoweredUp() call within a later release. I mean, Windows will be always WinRT and Linux will be bluez realistically. Should be branchable and auto-addable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent idea

;

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();
rickjansen-dev marked this conversation as resolved.
Show resolved Hide resolved

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)
rickjansen-dev marked this conversation as resolved.
Show resolved Hide resolved
{
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why adding it to the collection before querying the manufacturerdata

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, this is related to using certain property changes (RSSI in this case) as the trigger for the discovery handler callback. It could very well be that the manufacturer data comes along with other properties directly during the discovery. If the device is not added to the list before the discovery handler is invoked, the GetDeviceAsync will be called and it would not be able to return the device because it's not in the list of devices.

I know this is not pretty, plus come to think of it, adding it to the list might actually need to be done before the call to Initialize(), i'll have to check that to make sure it always works

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same problem during my use of BlueGiga-adapter: The advertising-packets from the BLE-device(Hubs) are coming in an "unordered" sequence. You've got to fetch them all until you're sure to have alos the manufacturer-data (because this is needed in Bluetooth-kernel of poweredup).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same problem during my use of BlueGiga-adapter: The advertising-packets from the BLE-device(Hubs) are coming in an "unordered" sequence. You've got to fetch them all until you're sure to have alos the manufacturer-data (because this is needed in Bluetooth-kernel of poweredup).

I'm not familiar with the bluegiga adapter, does that even use bluez? i'm unsure if unordered sequence of advertisement packets is an actual issue here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the BlueGiga-adapter ("dongle") BLED112 is a serial-device attached to USB. The client (poweredup-program here) talks to it just over a Serial-object. So it doesn't need Bluez or any other BLE-stack in the operating system, because the device itself handles the BLE-communication (timing and alike)

}
}

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